-
module ApplicationCable
-
class Channel < ActionCable::Channel::Base
-
end
-
end
-
module ApplicationCable
-
class Connection < ActionCable::Connection::Base
-
end
-
end
-
1
class ApplicationController < ActionController::Base
-
1
include RouteHelper
-
1
before_action :setup_gon
-
1
rescue_from Exception, java.lang.Throwable, :with => :internal_error
-
-
1
def internal_error(exception)
-
$log.error{LEX("An unhandled exception occurred!", exception)}
-
end
-
-
1
def setup_gon
-
2
gon.routes = setup_routes
-
2
gon.packs = packed_assets
-
2
gon.user = ""
-
end
-
end
-
1
module RouteHelper
-
1
include Webpacker::Helper
-
1
include ActionView::Helpers::AssetUrlHelper
-
-
1
IMAGE_EXTENSIONS = %w(.jpeg .jpg .png .gif).freeze
-
1
IMAGE_ROOT_PATH = 'media/packs/images/'
-
-
1
def setup_routes
-
2
original_verbosity = $VERBOSE
-
2
$VERBOSE = nil
-
2
routes = Rails.application.routes.named_routes.helper_names - ['rails_blob_path', 'rails_blob_url', 'rails_representation_path', 'rails_representation_url']
-
2
$VERBOSE = original_verbosity
-
2
@@routes_hash ||= {}
-
2
if @@routes_hash.empty?
-
1
routes.each do |route|
-
28
begin
-
28
@@routes_hash[route] = self.send(route)
-
rescue ActionController::UrlGenerationError => ex
-
8
if (ex.message =~ /missing required keys: \[(.*?)\]/)
-
8
keys = $1
-
8
keys = keys.split(',')
-
8
keys.map! do |e|
-
16
e.gsub!(':', '')
-
16
e.strip
-
end
-
8
required_keys_hash = {}
-
8
keys.each do |key|
-
16
required_keys_hash[key.to_sym] = ':' + key.to_s
-
end
-
8
@@routes_hash[route] = self.send(route, required_keys_hash)
-
else
-
raise ex
-
end
-
end
-
end
-
end
-
#$log.debug('routes hash passed to javascript is ' + @@routes_hash.to_s)
-
2
@@routes_hash
-
end
-
-
1
def setup_packed_assets
-
if Webpacker.instance.config.cache_manifest?
-
@@packed_assets ||= packed_assets
-
else
-
@@packed_assets = packed_assets
-
end
-
end
-
-
1
private
-
-
1
def packed_assets
-
2
h = {}
-
2
h[:paths] = {}
-
2
h[:urls] = {}
-
2
h[:urls][:images] = {}
-
2
h[:paths][:images] = {}
-
2
Webpacker.instance.manifest.refresh.each_pair do |k,v|
-
1016
unless k =~ /map$|entrypoints$/
-
838
url = asset_pack_url k
-
838
path = asset_pack_path k
-
838
h[:urls][k] = url
-
838
h[:paths][k] = path
-
838
if IMAGE_EXTENSIONS.include?(File.extname k)
-
44
rootless = k.sub(IMAGE_ROOT_PATH,'')
-
44
h[:urls][:images][k] = url
-
44
h[:urls][:images][rootless] = url
-
44
h[:paths][:images][k] = path
-
44
h[:paths][:images][rootless] = path
-
end
-
end
-
end
-
2
h
-
end
-
-
end
-
=begin
-
Webpacker.instance.manifest
-
Webpacker.instance.manifest.refresh #gives hash
-
include Webpacker::Helper
-
include ActionView::Helpers::AssetUrlHelper
-
include ActionView::Helpers::AssetTagHelper
-
Webpacker.instance.config.cache_manifest?
-
-
=end
-
class LayoutsController < ApplicationController
-
# this is the root route for the application
-
def root
-
end
-
-
def form_inputs
-
$log.always("Received " + params.inspect)
-
render json: {recieved: params}
-
end
-
end
-
1
class ScheduleController < ApplicationController
-
1
def get_facilities
-
# data = Facility.all
-
1
data = [{id: 1, location: 'New York'}, {id: 2, location: 'Los Angeles'}, {id: 3, location: 'Portland'}]
-
2
$log.info{"Get facilities is returning #{data.inspect}"}
-
1
render json: data
-
end
-
-
1
def get_appointment_types
-
# appointment_types = AppointmentType.all
-
1
appointment_types = [{id: 1, type: 'Allergist'}, {id: 2, type: 'Surgeon'}, {id: 3, type: 'Primary Care'}, {id:4, type: 'Cardiologist'}]
-
2
$log.info{"Get appointment types is returning #{appointment_types.inspect}"}
-
1
render json: appointment_types
-
end
-
-
=begin
-
def get_doctors
-
facility_id = params[:facility_id]
-
appointment_type_id = params[:appointment_type_id]
-
-
list = Doctor.where(appointment_type_id: appointment_type_id, facility_id: facility_id).all.to_a
-
$log.info{"Get doctors is returning #{list.inspect}"}
-
-
render json: list
-
end
-
=end
-
-
end
-
1
module ApplicationHelper
-
end
-
class ApplicationJob < ActiveJob::Base
-
end
-
class ApplicationMailer < ActionMailer::Base
-
default from: 'from@example.com'
-
layout 'mailer'
-
end
-
class ApplicationRecord < ActiveRecord::Base
-
self.abstract_class = true
-
end
-
1
require 'logging'
-
1
require 'fileutils'
-
1
require 'socket'
-
-
# if nil we are in trinidad
-
1
CATALINA_HOME = java.lang.System.properties['catalina.home']
-
1
subdir = (File.open('../context.txt').read.reverse.chop.reverse + '/') rescue ''
-
1
LOG_HOME = CATALINA_HOME.nil? ? "#{Rails.root}/logs/#{subdir}" : "#{CATALINA_HOME}/logs/#{subdir}"
-
1
FileUtils::mkdir_p LOG_HOME
-
1
Logging.basepath = Rails.root.to_s
-
-
-
1
module Logging
-
#add a level here if needed....
-
1
RAILS_COMMON_LEVELS = [:trace, :debug, :info, :warn, :error, :fatal, :unknown, :always]
-
1
def self.trace(exception)
-
trace_str = "\n"
-
if exception.respond_to? :backtrace
-
trace_str << exception.to_s << "\n"
-
unless exception.backtrace.nil?
-
trace_str << exception.backtrace.join("\n")
-
end
-
end
-
end
-
end
-
#Logging.caller_tracing=true
-
1
Logging.init *Logging::RAILS_COMMON_LEVELS
-
-
1
Logging.color_scheme('pretty',
-
levels: {
-
:info => :green,
-
:warn => :yellow,
-
:error => :red,
-
:fatal => [:white, :on_red],
-
:unknown => [:yellow, :on_blue],
-
:always => :white
-
},
-
date: :yellow,
-
#logger: :cyan,
-
#message: :magenta,
-
file: :magenta,
-
line: :cyan
-
)
-
-
1
color_scheme = WINDOWS ? 'pretty' : :default
-
-
#move pattern to prop file
-
1
pattern = $PROPS['LOG.pattern']
-
2
Logging.appenders.stdout(
-
'stdout',
-
:layout => Logging.layouts.pattern(
-
:pattern => pattern,
-
:color_scheme => color_scheme
-
)
-
)
-
-
2
rf = Logging.appenders.rolling_file(
-
'file',
-
layout: Logging.layouts.pattern(
-
pattern: pattern,
-
color_scheme: color_scheme,
-
# backtrace: true
-
),
-
roll_by: $PROPS['LOG.roll_by'],
-
keep: $PROPS['LOG.keep'].to_i,
-
age: $PROPS['LOG.age'],
-
filename: LOG_HOME + $PROPS['LOG.filename'],
-
truncate: true
-
)
-
-
2
error_appender = Logging.appenders.rolling_file(
-
'file',
-
layout: Logging.layouts.pattern(
-
pattern: pattern,
-
color_scheme: color_scheme,
-
),
-
roll_by: $PROPS['LOG.roll_by'],
-
keep: $PROPS['LOG.keep'].to_i,
-
age: $PROPS['LOG.age'],
-
filename: LOG_HOME + $PROPS['LOG.filename_error'],
-
truncate: true
-
)
-
-
1
class ErrorFilter < ::Logging::Filter
-
-
1
def initialize
-
9
@levels_hash = Logging::LEVELS.invert.map do |k,v| [k, v.to_sym] end.to_h
-
end
-
-
1
def allow(event)
-
3
allowed = @levels_hash[event.level].eql?(:error) || @levels_hash[event.level].eql?(:fatal)
-
3
allowed ? event : nil
-
end
-
-
end
-
#error_appender.level = :error
-
1
error_appender.filters=ErrorFilter.new
-
-
1
begin
-
-
1
$log = ::Logging::Logger['MainLogger']
-
1
$log.caller_tracing=$PROPS['LOG.caller_tracing'].upcase.eql?('TRUE')
-
-
1
$log.add_appenders 'stdout' if ($PROPS['LOG.append_stdout'].upcase.eql?('TRUE'))
-
1
$log.add_appenders rf
-
#$log.add_appenders error_appender
-
1
$log.level = $PROPS['LOG.level'].downcase.to_sym
-
-
1
unless $PROPS['LOG.filename_admin'].nil?
-
-
#rf_rails is for rails logging
-
rf_admin = Logging.appenders.rolling_file(
-
'file',
-
layout: Logging.layouts.pattern(
-
pattern: pattern,
-
color_scheme: color_scheme,
-
# backtrace: true
-
),
-
roll_by: $PROPS['LOG.roll_by'],
-
keep: $PROPS['LOG.keep'].to_i,
-
age: $PROPS['LOG.age'],
-
filename: LOG_HOME + $PROPS['LOG.filename_admin'],
-
truncate: true
-
)
-
$alog = ::Logging::Logger['LogAdmin']
-
$alog.caller_tracing=$PROPS['LOG.caller_tracing'].upcase.eql?('TRUE')
-
-
$alog.add_appenders 'stdout' if $PROPS['LOG.append_stdout'].upcase.eql?('TRUE')
-
$alog.add_appenders rf_admin
-
$alog.level = $PROPS['LOG.level'].downcase.to_sym
-
end
-
-
1
ALL_LOGGERS = [$log, $alog].reject(&:nil?).freeze
-
# these log messages will be nicely colored
-
# the level will be colored differently for each message
-
# PrismeLogEvent not visible yet
-
1
unless (File.basename($0) == 'rake')
-
ALL_LOGGERS.each {|e| e.always 'Logging started!'}
-
end
-
rescue => ex
-
warn "Logger failed to initialize. Reason is #{ex.to_s}"
-
warn ex.backtrace.join("\n")
-
warn 'Shutting down the web server!'
-
java.lang.System.exit(1)
-
end
-
-
1
ALL_LOGGERS.each do |logger|
-
1
logger.add_appenders error_appender
-
end
-
-
#WARNING, using these methods doesn't produce the correct file location in the logs.
-
1
ALL_LOGGERS.each do |logger|
-
1
Logging::RAILS_COMMON_LEVELS.each do |level|
-
8
method_name = ("#{level}_e").to_sym
-
8
logger.define_singleton_method(method_name) do |message, exception|
-
logger.send(level, message.to_s)
-
if exception.respond_to? :backtrace
-
logger.send(level, exception.to_s)
-
logger.send(level, exception.backtrace.join("\n")) unless exception.backtrace.nil?
-
end
-
end
-
end
-
end
-
-
1
module Kernel
-
1
def LEX(message, exception)
-
result = "#{message}\n#{exception.class}: #{exception.message}\n#{exception.backtrace&.join("\n")}" rescue message.to_s
-
result
-
end
-
1
module_function :LEX
-
end
-
1
$log.always "Using color scheme #{color_scheme}, Rails mode is #{Rails.env}"
-
=begin
-
load('lib/logging/logging.rb')
-
$log.debug{LEX("I had a boo boo 2", ex)}
-
=end
-
1
require 'erb'
-
-
1
module PropLoader
-
1
extend self
-
-
1
class << self
-
1
attr_accessor :props
-
end
-
-
1
def self.load_prop_files(*dirs)
-
1
@props = {}
-
1
dirs.each do |dir|
-
# iterate over all of the .properties files in the directory
-
1
Dir.glob("#{dir}/*.properties*") do |file|
-
1
key_prefix = File.basename(file).split(".")[0].upcase
-
-
1
if File.extname(file).eql?('.erb')
-
1
props = self.read_props_from_erb(file, key_prefix)
-
else
-
# read the file line by line stripping out properties
-
props = read_prop_file(file, key_prefix)
-
end
-
1
@props.merge!(props)
-
end
-
end
-
end
-
-
1
def self.reload
-
1
PropLoader.load_prop_files('./config/props')
-
1
$PROPS = PropLoader.props.clone
-
1
$PROPS.freeze
-
end
-
-
1
private
-
1
def self.read_prop_file(file, key_prefix)
-
ret = {}
-
-
File.readlines(file).each do |line|
-
r = read_prop_line(line, key_prefix)
-
ret.merge!(r)
-
end
-
ret
-
end
-
-
1
def self.read_props_from_erb(erb, key_prefix)
-
2
props = ERB.new(File.open(erb, 'r') { |file| file.read }).result
-
1
properties = {}
-
1
prop_array = props.split("\n")
-
1
prop_array.each do |line|
-
27
properties.merge!(read_prop_line(line, key_prefix))
-
end
-
1
properties
-
end
-
-
1
def self.read_prop_line(line, key_prefix)
-
27
properties = {}
-
27
line.strip!
-
27
return properties if line.eql?("")
-
22
if (line[0] != ?# and line[0] != ?=)
-
11
i = line.index('=')
-
11
if (i)
-
11
properties["#{key_prefix}." + line[0..i - 1].strip] = line[i + 1..-1].strip
-
else
-
properties["#{key_prefix}."+line] = ''
-
end
-
end
-
22
properties
-
end
-
end
-
-
1
PropLoader.reload
-
require 'uri'
-
-
module Utilities
-
TMP_FILE_PREFIX = './tmp/'
-
YML_EXT = '.yml'
-
MAVEN_TARGET_DIRECTORY = './target'
-
##
-
# this method takes a camel cased word and changes it to snake case
-
# Example: RailsKomet -> rails_komet
-
#
-
def to_snake_case(camel_cased_word)
-
camel_cased_word.to_s.gsub(/::/, '/').
-
gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2').
-
gsub(/([a-z\d])([A-Z])/, '\1_\2').
-
tr('-', '_').
-
downcase
-
end
-
-
##
-
# writes the json data to a tmp file based on the filename passed
-
# @param json - the JSON data to write out
-
# @param file_name - the filename to write out to the /tmp directory
-
def json_to_yaml_file(json, file_name)
-
if Rails.env.development?
-
prefix = '#Fixture created on ' + Time.now.strftime('%F %H:%M:%S') + "\n"
-
File.write("#{TMP_FILE_PREFIX}#{file_name}" + YML_EXT, prefix + json.to_yaml)
-
#$log.trace("Writing yaml file #{TMP_FILE_PREFIX}#{file_name}.yml.")
-
else
-
#$log.trace("Not writing yaml file #{TMP_FILE_PREFIX}#{file_name}.yml. Rails.env = #{Rails.env}")
-
end
-
-
end
-
-
##
-
# Convert the URL to a string for use with the json_to_yaml_file method call
-
# @param url - the URL path to convert to a string with underscores
-
# @return - the filename based on the URL passed
-
def url_to_path_string(url)
-
url = url.clone
-
begin
-
url.gsub!('{', '') #reduce paths like http://www.google.com/foo/{id}/faa to http://www.google.com/foo/id/faa
-
url.gsub!('}', '') #reduce paths like http://www.google.com/foo/{id}/faa to http://www.google.com/foo/id/faa
-
path = URI(url).path.gsub('/', '_')
-
path = 'no_path' if path.empty?
-
return path
-
rescue => ex
-
#$log.error('An invalid matched_url was given!')
-
#$log.error(ex)
-
end
-
'bad_url'
-
end
-
end
-
-
module Kernel
-
TRUE_VALS = %w(true t yes y on 1)
-
FALSE_VALS = %w(false f no n off 0)
-
-
def boolean(boolean_string)
-
val = boolean_string.to_s.downcase.gsub(/\s+/, '')
-
return false if val.empty?
-
return true if TRUE_VALS.include?(val)
-
return false if FALSE_VALS.include?(val)
-
raise ArgumentError.new("invalid value for Boolean: \"#{val}\"")
-
end
-
-
def gov
-
Java::Gov
-
end
-
-
end
-
-
class String
-
# similar to the camelize in rails, but it only mutates the first character after the underscore
-
# Suppose you have the java method 'getInterfaceEngineURL', note how the last set of characters are all uppercase
-
# "interface_engine_URL".camelize() => "InterfaceEngineUrl"
-
# "interface_engine_URL".camelize_preserving => "InterfaceEngineURL"
-
# "interface_engine_URL".camelize_preserving(false) => "interfaceEngineURL"
-
def camelize_preserving(modify_first_letter = true)
-
return self.split('_').each_with_index.collect do |e, i|
-
if ((i == 0) && !modify_first_letter)
-
else
-
e[0] = e[0].capitalize
-
end
-
e
-
end.join
-
end
-
-
def to_b
-
boolean(self)
-
end
-
-
def os_path!
-
self.gsub!('/', java.io.File::separator)
-
self.gsub!('\\', java.io.File::separator)
-
end
-
end
-
-
module JSON
-
class << self
-
def indifferent_parse(source, opts = {})
-
HashWithIndifferentAccess.new(JSON.parse(source, opts))
-
end
-
end
-
end
-
-
module FileHelper
-
def FileHelper.file_as_string(file)
-
rVal = ''
-
File.open(file, 'r') do |file_handle|
-
file_handle.read.each_line do |line|
-
rVal << line
-
end
-
end
-
rVal
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActionCable
-
1
module Channel
-
1
extend ActiveSupport::Autoload
-
-
1
eager_autoload do
-
1
autoload :Base
-
1
autoload :Broadcasting
-
1
autoload :Callbacks
-
1
autoload :Naming
-
1
autoload :PeriodicTimers
-
1
autoload :Streams
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "set"
-
-
1
module ActionCable
-
1
module Channel
-
# The channel provides the basic structure of grouping behavior into logical units when communicating over the WebSocket connection.
-
# You can think of a channel like a form of controller, but one that's capable of pushing content to the subscriber in addition to simply
-
# responding to the subscriber's direct requests.
-
#
-
# Channel instances are long-lived. A channel object will be instantiated when the cable consumer becomes a subscriber, and then
-
# lives until the consumer disconnects. This may be seconds, minutes, hours, or even days. That means you have to take special care
-
# not to do anything silly in a channel that would balloon its memory footprint or whatever. The references are forever, so they won't be released
-
# as is normally the case with a controller instance that gets thrown away after every request.
-
#
-
# Long-lived channels (and connections) also mean you're responsible for ensuring that the data is fresh. If you hold a reference to a user
-
# record, but the name is changed while that reference is held, you may be sending stale data if you don't take precautions to avoid it.
-
#
-
# The upside of long-lived channel instances is that you can use instance variables to keep reference to objects that future subscriber requests
-
# can interact with. Here's a quick example:
-
#
-
# class ChatChannel < ApplicationCable::Channel
-
# def subscribed
-
# @room = Chat::Room[params[:room_number]]
-
# end
-
#
-
# def speak(data)
-
# @room.speak data, user: current_user
-
# end
-
# end
-
#
-
# The #speak action simply uses the Chat::Room object that was created when the channel was first subscribed to by the consumer when that
-
# subscriber wants to say something in the room.
-
#
-
# == Action processing
-
#
-
# Unlike subclasses of ActionController::Base, channels do not follow a RESTful
-
# constraint form for their actions. Instead, Action Cable operates through a
-
# remote-procedure call model. You can declare any public method on the
-
# channel (optionally taking a <tt>data</tt> argument), and this method is
-
# automatically exposed as callable to the client.
-
#
-
# Example:
-
#
-
# class AppearanceChannel < ApplicationCable::Channel
-
# def subscribed
-
# @connection_token = generate_connection_token
-
# end
-
#
-
# def unsubscribed
-
# current_user.disappear @connection_token
-
# end
-
#
-
# def appear(data)
-
# current_user.appear @connection_token, on: data['appearing_on']
-
# end
-
#
-
# def away
-
# current_user.away @connection_token
-
# end
-
#
-
# private
-
# def generate_connection_token
-
# SecureRandom.hex(36)
-
# end
-
# end
-
#
-
# In this example, the subscribed and unsubscribed methods are not callable methods, as they
-
# were already declared in ActionCable::Channel::Base, but <tt>#appear</tt>
-
# and <tt>#away</tt> are. <tt>#generate_connection_token</tt> is also not
-
# callable, since it's a private method. You'll see that appear accepts a data
-
# parameter, which it then uses as part of its model call. <tt>#away</tt>
-
# does not, since it's simply a trigger action.
-
#
-
# Also note that in this example, <tt>current_user</tt> is available because
-
# it was marked as an identifying attribute on the connection. All such
-
# identifiers will automatically create a delegation method of the same name
-
# on the channel instance.
-
#
-
# == Rejecting subscription requests
-
#
-
# A channel can reject a subscription request in the #subscribed callback by
-
# invoking the #reject method:
-
#
-
# class ChatChannel < ApplicationCable::Channel
-
# def subscribed
-
# @room = Chat::Room[params[:room_number]]
-
# reject unless current_user.can_access?(@room)
-
# end
-
# end
-
#
-
# In this example, the subscription will be rejected if the
-
# <tt>current_user</tt> does not have access to the chat room. On the
-
# client-side, the <tt>Channel#rejected</tt> callback will get invoked when
-
# the server rejects the subscription request.
-
1
class Base
-
1
include Callbacks
-
1
include PeriodicTimers
-
1
include Streams
-
1
include Naming
-
1
include Broadcasting
-
-
1
attr_reader :params, :connection, :identifier
-
1
delegate :logger, to: :connection
-
-
1
class << self
-
# A list of method names that should be considered actions. This
-
# includes all public instance methods on a channel, less
-
# any internal methods (defined on Base), adding back in
-
# any methods that are internal, but still exist on the class
-
# itself.
-
#
-
# ==== Returns
-
# * <tt>Set</tt> - A set of all methods that should be considered actions.
-
1
def action_methods
-
@action_methods ||= begin
-
# All public instance methods of this class, including ancestors
-
methods = (public_instance_methods(true) -
-
# Except for public instance methods of Base and its ancestors
-
ActionCable::Channel::Base.public_instance_methods(true) +
-
# Be sure to include shadowed public instance methods of this class
-
public_instance_methods(false)).uniq.map(&:to_s)
-
methods.to_set
-
end
-
end
-
-
1
private
-
# action_methods are cached and there is sometimes need to refresh
-
# them. ::clear_action_methods! allows you to do that, so next time
-
# you run action_methods, they will be recalculated.
-
1
def clear_action_methods! # :doc:
-
21
@action_methods = nil
-
end
-
-
# Refresh the cached action_methods when a new action_method is added.
-
1
def method_added(name) # :doc:
-
21
super
-
21
clear_action_methods!
-
end
-
end
-
-
1
def initialize(connection, identifier, params = {})
-
@connection = connection
-
@identifier = identifier
-
@params = params
-
-
# When a channel is streaming via pubsub, we want to delay the confirmation
-
# transmission until pubsub subscription is confirmed.
-
#
-
# The counter starts at 1 because it's awaiting a call to #subscribe_to_channel
-
@defer_subscription_confirmation_counter = Concurrent::AtomicFixnum.new(1)
-
-
@reject_subscription = nil
-
@subscription_confirmation_sent = nil
-
-
delegate_connection_identifiers
-
end
-
-
# Extract the action name from the passed data and process it via the channel. The process will ensure
-
# that the action requested is a public method on the channel declared by the user (so not one of the callbacks
-
# like #subscribed).
-
1
def perform_action(data)
-
action = extract_action(data)
-
-
if processable_action?(action)
-
payload = { channel_class: self.class.name, action: action, data: data }
-
ActiveSupport::Notifications.instrument("perform_action.action_cable", payload) do
-
dispatch_action(action, data)
-
end
-
else
-
logger.error "Unable to process #{action_signature(action, data)}"
-
end
-
end
-
-
# This method is called after subscription has been added to the connection
-
# and confirms or rejects the subscription.
-
1
def subscribe_to_channel
-
run_callbacks :subscribe do
-
subscribed
-
end
-
-
reject_subscription if subscription_rejected?
-
ensure_confirmation_sent
-
end
-
-
# Called by the cable connection when it's cut, so the channel has a chance to cleanup with callbacks.
-
# This method is not intended to be called directly by the user. Instead, overwrite the #unsubscribed callback.
-
1
def unsubscribe_from_channel # :nodoc:
-
run_callbacks :unsubscribe do
-
unsubscribed
-
end
-
end
-
-
1
private
-
# Called once a consumer has become a subscriber of the channel. Usually the place to setup any streams
-
# you want this channel to be sending to the subscriber.
-
1
def subscribed # :doc:
-
# Override in subclasses
-
end
-
-
# Called once a consumer has cut its cable connection. Can be used for cleaning up connections or marking
-
# users as offline or the like.
-
1
def unsubscribed # :doc:
-
# Override in subclasses
-
end
-
-
# Transmit a hash of data to the subscriber. The hash will automatically be wrapped in a JSON envelope with
-
# the proper channel identifier marked as the recipient.
-
1
def transmit(data, via: nil) # :doc:
-
status = "#{self.class.name} transmitting #{data.inspect.truncate(300)}"
-
status += " (via #{via})" if via
-
logger.debug(status)
-
-
payload = { channel_class: self.class.name, data: data, via: via }
-
ActiveSupport::Notifications.instrument("transmit.action_cable", payload) do
-
connection.transmit identifier: @identifier, message: data
-
end
-
end
-
-
1
def ensure_confirmation_sent # :doc:
-
return if subscription_rejected?
-
@defer_subscription_confirmation_counter.decrement
-
transmit_subscription_confirmation unless defer_subscription_confirmation?
-
end
-
-
1
def defer_subscription_confirmation! # :doc:
-
@defer_subscription_confirmation_counter.increment
-
end
-
-
1
def defer_subscription_confirmation? # :doc:
-
@defer_subscription_confirmation_counter.value > 0
-
end
-
-
1
def subscription_confirmation_sent? # :doc:
-
@subscription_confirmation_sent
-
end
-
-
1
def reject # :doc:
-
@reject_subscription = true
-
end
-
-
1
def subscription_rejected? # :doc:
-
@reject_subscription
-
end
-
-
1
def delegate_connection_identifiers
-
connection.identifiers.each do |identifier|
-
define_singleton_method(identifier) do
-
connection.send(identifier)
-
end
-
end
-
end
-
-
1
def extract_action(data)
-
(data["action"].presence || :receive).to_sym
-
end
-
-
1
def processable_action?(action)
-
self.class.action_methods.include?(action.to_s) unless subscription_rejected?
-
end
-
-
1
def dispatch_action(action, data)
-
logger.info action_signature(action, data)
-
-
if method(action).arity == 1
-
public_send action, data
-
else
-
public_send action
-
end
-
end
-
-
1
def action_signature(action, data)
-
"#{self.class.name}##{action}".dup.tap do |signature|
-
if (arguments = data.except("action")).any?
-
signature << "(#{arguments.inspect})"
-
end
-
end
-
end
-
-
1
def transmit_subscription_confirmation
-
unless subscription_confirmation_sent?
-
logger.info "#{self.class.name} is transmitting the subscription confirmation"
-
-
ActiveSupport::Notifications.instrument("transmit_subscription_confirmation.action_cable", channel_class: self.class.name) do
-
connection.transmit identifier: @identifier, type: ActionCable::INTERNAL[:message_types][:confirmation]
-
@subscription_confirmation_sent = true
-
end
-
end
-
end
-
-
1
def reject_subscription
-
connection.subscriptions.remove_subscription self
-
transmit_subscription_rejection
-
end
-
-
1
def transmit_subscription_rejection
-
logger.info "#{self.class.name} is transmitting the subscription rejection"
-
-
ActiveSupport::Notifications.instrument("transmit_subscription_rejection.action_cable", channel_class: self.class.name) do
-
connection.transmit identifier: @identifier, type: ActionCable::INTERNAL[:message_types][:rejection]
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_support/core_ext/object/to_param"
-
-
1
module ActionCable
-
1
module Channel
-
1
module Broadcasting
-
1
extend ActiveSupport::Concern
-
-
1
delegate :broadcasting_for, to: :class
-
-
1
module ClassMethods
-
# Broadcast a hash to a unique broadcasting for this <tt>model</tt> in this channel.
-
1
def broadcast_to(model, message)
-
ActionCable.server.broadcast(broadcasting_for([ channel_name, model ]), message)
-
end
-
-
1
def broadcasting_for(model) #:nodoc:
-
case
-
when model.is_a?(Array)
-
model.map { |m| broadcasting_for(m) }.join(":")
-
when model.respond_to?(:to_gid_param)
-
model.to_gid_param
-
else
-
model.to_param
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_support/callbacks"
-
-
1
module ActionCable
-
1
module Channel
-
1
module Callbacks
-
1
extend ActiveSupport::Concern
-
1
include ActiveSupport::Callbacks
-
-
1
included do
-
1
define_callbacks :subscribe
-
1
define_callbacks :unsubscribe
-
end
-
-
1
module ClassMethods
-
1
def before_subscribe(*methods, &block)
-
set_callback(:subscribe, :before, *methods, &block)
-
end
-
-
1
def after_subscribe(*methods, &block)
-
1
set_callback(:subscribe, :after, *methods, &block)
-
end
-
1
alias_method :on_subscribe, :after_subscribe
-
-
1
def before_unsubscribe(*methods, &block)
-
set_callback(:unsubscribe, :before, *methods, &block)
-
end
-
-
1
def after_unsubscribe(*methods, &block)
-
2
set_callback(:unsubscribe, :after, *methods, &block)
-
end
-
1
alias_method :on_unsubscribe, :after_unsubscribe
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActionCable
-
1
module Channel
-
1
module Naming
-
1
extend ActiveSupport::Concern
-
-
1
module ClassMethods
-
# Returns the name of the channel, underscored, without the <tt>Channel</tt> ending.
-
# If the channel is in a namespace, then the namespaces are represented by single
-
# colon separators in the channel name.
-
#
-
# ChatChannel.channel_name # => 'chat'
-
# Chats::AppearancesChannel.channel_name # => 'chats:appearances'
-
# FooChats::BarAppearancesChannel.channel_name # => 'foo_chats:bar_appearances'
-
1
def channel_name
-
@channel_name ||= name.sub(/Channel$/, "").gsub("::", ":").underscore
-
end
-
end
-
-
# Delegates to the class' <tt>channel_name</tt>
-
1
delegate :channel_name, to: :class
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActionCable
-
1
module Channel
-
1
module PeriodicTimers
-
1
extend ActiveSupport::Concern
-
-
1
included do
-
1
class_attribute :periodic_timers, instance_reader: false, default: []
-
-
1
after_subscribe :start_periodic_timers
-
1
after_unsubscribe :stop_periodic_timers
-
end
-
-
1
module ClassMethods
-
# Periodically performs a task on the channel, like updating an online
-
# user counter, polling a backend for new status messages, sending
-
# regular "heartbeat" messages, or doing some internal work and giving
-
# progress updates.
-
#
-
# Pass a method name or lambda argument or provide a block to call.
-
# Specify the calling period in seconds using the <tt>every:</tt>
-
# keyword argument.
-
#
-
# periodically :transmit_progress, every: 5.seconds
-
#
-
# periodically every: 3.minutes do
-
# transmit action: :update_count, count: current_count
-
# end
-
#
-
1
def periodically(callback_or_method_name = nil, every:, &block)
-
callback =
-
if block_given?
-
raise ArgumentError, "Pass a block or provide a callback arg, not both" if callback_or_method_name
-
block
-
else
-
case callback_or_method_name
-
when Proc
-
callback_or_method_name
-
when Symbol
-
-> { __send__ callback_or_method_name }
-
else
-
raise ArgumentError, "Expected a Symbol method name or a Proc, got #{callback_or_method_name.inspect}"
-
end
-
end
-
-
unless every.kind_of?(Numeric) && every > 0
-
raise ArgumentError, "Expected every: to be a positive number of seconds, got #{every.inspect}"
-
end
-
-
self.periodic_timers += [[ callback, every: every ]]
-
end
-
end
-
-
1
private
-
1
def active_periodic_timers
-
@active_periodic_timers ||= []
-
end
-
-
1
def start_periodic_timers
-
self.class.periodic_timers.each do |callback, options|
-
active_periodic_timers << start_periodic_timer(callback, every: options.fetch(:every))
-
end
-
end
-
-
1
def start_periodic_timer(callback, every:)
-
connection.server.event_loop.timer every do
-
connection.worker_pool.async_exec self, connection: connection, &callback
-
end
-
end
-
-
1
def stop_periodic_timers
-
active_periodic_timers.each { |timer| timer.shutdown }
-
active_periodic_timers.clear
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActionCable
-
1
module Channel
-
# Streams allow channels to route broadcastings to the subscriber. A broadcasting is, as discussed elsewhere, a pubsub queue where any data
-
# placed into it is automatically sent to the clients that are connected at that time. It's purely an online queue, though. If you're not
-
# streaming a broadcasting at the very moment it sends out an update, you will not get that update, even if you connect after it has been sent.
-
#
-
# Most commonly, the streamed broadcast is sent straight to the subscriber on the client-side. The channel just acts as a connector between
-
# the two parties (the broadcaster and the channel subscriber). Here's an example of a channel that allows subscribers to get all new
-
# comments on a given page:
-
#
-
# class CommentsChannel < ApplicationCable::Channel
-
# def follow(data)
-
# stream_from "comments_for_#{data['recording_id']}"
-
# end
-
#
-
# def unfollow
-
# stop_all_streams
-
# end
-
# end
-
#
-
# Based on the above example, the subscribers of this channel will get whatever data is put into the,
-
# let's say, <tt>comments_for_45</tt> broadcasting as soon as it's put there.
-
#
-
# An example broadcasting for this channel looks like so:
-
#
-
# ActionCable.server.broadcast "comments_for_45", author: 'DHH', content: 'Rails is just swell'
-
#
-
# If you have a stream that is related to a model, then the broadcasting used can be generated from the model and channel.
-
# The following example would subscribe to a broadcasting like <tt>comments:Z2lkOi8vVGVzdEFwcC9Qb3N0LzE</tt>.
-
#
-
# class CommentsChannel < ApplicationCable::Channel
-
# def subscribed
-
# post = Post.find(params[:id])
-
# stream_for post
-
# end
-
# end
-
#
-
# You can then broadcast to this channel using:
-
#
-
# CommentsChannel.broadcast_to(@post, @comment)
-
#
-
# If you don't just want to parlay the broadcast unfiltered to the subscriber, you can also supply a callback that lets you alter what is sent out.
-
# The below example shows how you can use this to provide performance introspection in the process:
-
#
-
# class ChatChannel < ApplicationCable::Channel
-
# def subscribed
-
# @room = Chat::Room[params[:room_number]]
-
#
-
# stream_for @room, coder: ActiveSupport::JSON do |message|
-
# if message['originated_at'].present?
-
# elapsed_time = (Time.now.to_f - message['originated_at']).round(2)
-
#
-
# ActiveSupport::Notifications.instrument :performance, measurement: 'Chat.message_delay', value: elapsed_time, action: :timing
-
# logger.info "Message took #{elapsed_time}s to arrive"
-
# end
-
#
-
# transmit message
-
# end
-
# end
-
# end
-
#
-
# You can stop streaming from all broadcasts by calling #stop_all_streams.
-
1
module Streams
-
1
extend ActiveSupport::Concern
-
-
1
included do
-
1
on_unsubscribe :stop_all_streams
-
end
-
-
# Start streaming from the named <tt>broadcasting</tt> pubsub queue. Optionally, you can pass a <tt>callback</tt> that'll be used
-
# instead of the default of just transmitting the updates straight to the subscriber.
-
# Pass <tt>coder: ActiveSupport::JSON</tt> to decode messages as JSON before passing to the callback.
-
# Defaults to <tt>coder: nil</tt> which does no decoding, passes raw messages.
-
1
def stream_from(broadcasting, callback = nil, coder: nil, &block)
-
broadcasting = String(broadcasting)
-
-
# Don't send the confirmation until pubsub#subscribe is successful
-
defer_subscription_confirmation!
-
-
# Build a stream handler by wrapping the user-provided callback with
-
# a decoder or defaulting to a JSON-decoding retransmitter.
-
handler = worker_pool_stream_handler(broadcasting, callback || block, coder: coder)
-
streams << [ broadcasting, handler ]
-
-
connection.server.event_loop.post do
-
pubsub.subscribe(broadcasting, handler, lambda do
-
ensure_confirmation_sent
-
logger.info "#{self.class.name} is streaming from #{broadcasting}"
-
end)
-
end
-
end
-
-
# Start streaming the pubsub queue for the <tt>model</tt> in this channel. Optionally, you can pass a
-
# <tt>callback</tt> that'll be used instead of the default of just transmitting the updates straight
-
# to the subscriber.
-
#
-
# Pass <tt>coder: ActiveSupport::JSON</tt> to decode messages as JSON before passing to the callback.
-
# Defaults to <tt>coder: nil</tt> which does no decoding, passes raw messages.
-
1
def stream_for(model, callback = nil, coder: nil, &block)
-
stream_from(broadcasting_for([ channel_name, model ]), callback || block, coder: coder)
-
end
-
-
# Unsubscribes all streams associated with this channel from the pubsub queue.
-
1
def stop_all_streams
-
streams.each do |broadcasting, callback|
-
pubsub.unsubscribe broadcasting, callback
-
logger.info "#{self.class.name} stopped streaming from #{broadcasting}"
-
end.clear
-
end
-
-
1
private
-
1
delegate :pubsub, to: :connection
-
-
1
def streams
-
@_streams ||= []
-
end
-
-
# Always wrap the outermost handler to invoke the user handler on the
-
# worker pool rather than blocking the event loop.
-
1
def worker_pool_stream_handler(broadcasting, user_handler, coder: nil)
-
handler = stream_handler(broadcasting, user_handler, coder: coder)
-
-
-> message do
-
connection.worker_pool.async_invoke handler, :call, message, connection: connection
-
end
-
end
-
-
# May be overridden to add instrumentation, logging, specialized error
-
# handling, or other forms of handler decoration.
-
#
-
# TODO: Tests demonstrating this.
-
1
def stream_handler(broadcasting, user_handler, coder: nil)
-
if user_handler
-
stream_decoder user_handler, coder: coder
-
else
-
default_stream_handler broadcasting, coder: coder
-
end
-
end
-
-
# May be overridden to change the default stream handling behavior
-
# which decodes JSON and transmits to the client.
-
#
-
# TODO: Tests demonstrating this.
-
#
-
# TODO: Room for optimization. Update transmit API to be coder-aware
-
# so we can no-op when pubsub and connection are both JSON-encoded.
-
# Then we can skip decode+encode if we're just proxying messages.
-
1
def default_stream_handler(broadcasting, coder:)
-
coder ||= ActiveSupport::JSON
-
stream_transmitter stream_decoder(coder: coder), broadcasting: broadcasting
-
end
-
-
1
def stream_decoder(handler = identity_handler, coder:)
-
if coder
-
-> message { handler.(coder.decode(message)) }
-
else
-
handler
-
end
-
end
-
-
1
def stream_transmitter(handler = identity_handler, broadcasting:)
-
via = "streamed from #{broadcasting}"
-
-
-> (message) do
-
transmit handler.(message), via: via
-
end
-
end
-
-
1
def identity_handler
-
-> message { message }
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActionCable
-
1
module Server
-
1
extend ActiveSupport::Autoload
-
-
1
eager_autoload do
-
1
autoload :Base
-
1
autoload :Broadcasting
-
1
autoload :Connections
-
1
autoload :Configuration
-
-
1
autoload :Worker
-
1
autoload :ActiveRecordConnectionManagement, "action_cable/server/worker/active_record_connection_management"
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "monitor"
-
-
1
module ActionCable
-
1
module Server
-
# A singleton ActionCable::Server instance is available via ActionCable.server. It's used by the Rack process that starts the Action Cable server, but
-
# is also used by the user to reach the RemoteConnections object, which is used for finding and disconnecting connections across all servers.
-
#
-
# Also, this is the server instance used for broadcasting. See Broadcasting for more information.
-
1
class Base
-
1
include ActionCable::Server::Broadcasting
-
1
include ActionCable::Server::Connections
-
-
1
cattr_accessor :config, instance_accessor: true, default: ActionCable::Server::Configuration.new
-
-
1
def self.logger; config.logger; end
-
1
delegate :logger, to: :config
-
-
1
attr_reader :mutex
-
-
1
def initialize
-
1
@mutex = Monitor.new
-
1
@remote_connections = @event_loop = @worker_pool = @pubsub = nil
-
end
-
-
# Called by Rack to setup the server.
-
1
def call(env)
-
setup_heartbeat_timer
-
config.connection_class.call.new(self, env).process
-
end
-
-
# Disconnect all the connections identified by +identifiers+ on this server or any others via RemoteConnections.
-
1
def disconnect(identifiers)
-
remote_connections.where(identifiers).disconnect
-
end
-
-
1
def restart
-
connections.each(&:close)
-
-
@mutex.synchronize do
-
# Shutdown the worker pool
-
@worker_pool.halt if @worker_pool
-
@worker_pool = nil
-
-
# Shutdown the pub/sub adapter
-
@pubsub.shutdown if @pubsub
-
@pubsub = nil
-
end
-
end
-
-
# Gateway to RemoteConnections. See that class for details.
-
1
def remote_connections
-
@remote_connections || @mutex.synchronize { @remote_connections ||= RemoteConnections.new(self) }
-
end
-
-
1
def event_loop
-
@event_loop || @mutex.synchronize { @event_loop ||= ActionCable::Connection::StreamEventLoop.new }
-
end
-
-
# The worker pool is where we run connection callbacks and channel actions. We do as little as possible on the server's main thread.
-
# The worker pool is an executor service that's backed by a pool of threads working from a task queue. The thread pool size maxes out
-
# at 4 worker threads by default. Tune the size yourself with <tt>config.action_cable.worker_pool_size</tt>.
-
#
-
# Using Active Record, Redis, etc within your channel actions means you'll get a separate connection from each thread in the worker pool.
-
# Plan your deployment accordingly: 5 servers each running 5 Puma workers each running an 8-thread worker pool means at least 200 database
-
# connections.
-
#
-
# Also, ensure that your database connection pool size is as least as large as your worker pool size. Otherwise, workers may oversubscribe
-
# the database connection pool and block while they wait for other workers to release their connections. Use a smaller worker pool or a larger
-
# database connection pool instead.
-
1
def worker_pool
-
@worker_pool || @mutex.synchronize { @worker_pool ||= ActionCable::Server::Worker.new(max_size: config.worker_pool_size) }
-
end
-
-
# Adapter used for all streams/broadcasting.
-
1
def pubsub
-
@pubsub || @mutex.synchronize { @pubsub ||= config.pubsub_adapter.new(self) }
-
end
-
-
# All of the identifiers applied to the connection class associated with this server.
-
1
def connection_identifiers
-
config.connection_class.call.identifiers
-
end
-
end
-
-
1
ActiveSupport.run_load_hooks(:action_cable, Base.config)
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActionCable
-
1
module Server
-
# Broadcasting is how other parts of your application can send messages to a channel's subscribers. As explained in Channel, most of the time, these
-
# broadcastings are streamed directly to the clients subscribed to the named broadcasting. Let's explain with a full-stack example:
-
#
-
# class WebNotificationsChannel < ApplicationCable::Channel
-
# def subscribed
-
# stream_from "web_notifications_#{current_user.id}"
-
# end
-
# end
-
#
-
# # Somewhere in your app this is called, perhaps from a NewCommentJob:
-
# ActionCable.server.broadcast \
-
# "web_notifications_1", { title: "New things!", body: "All that's fit for print" }
-
#
-
# # Client-side CoffeeScript, which assumes you've already requested the right to send web notifications:
-
# App.cable.subscriptions.create "WebNotificationsChannel",
-
# received: (data) ->
-
# new Notification data['title'], body: data['body']
-
1
module Broadcasting
-
# Broadcast a hash directly to a named <tt>broadcasting</tt>. This will later be JSON encoded.
-
1
def broadcast(broadcasting, message, coder: ActiveSupport::JSON)
-
broadcaster_for(broadcasting, coder: coder).broadcast(message)
-
end
-
-
# Returns a broadcaster for a named <tt>broadcasting</tt> that can be reused. Useful when you have an object that
-
# may need multiple spots to transmit to a specific broadcasting over and over.
-
1
def broadcaster_for(broadcasting, coder: ActiveSupport::JSON)
-
Broadcaster.new(self, String(broadcasting), coder: coder)
-
end
-
-
1
private
-
1
class Broadcaster
-
1
attr_reader :server, :broadcasting, :coder
-
-
1
def initialize(server, broadcasting, coder:)
-
@server, @broadcasting, @coder = server, broadcasting, coder
-
end
-
-
1
def broadcast(message)
-
server.logger.debug "[ActionCable] Broadcasting to #{broadcasting}: #{message.inspect}"
-
-
payload = { broadcasting: broadcasting, message: message, coder: coder }
-
ActiveSupport::Notifications.instrument("broadcast.action_cable", payload) do
-
encoded = coder ? coder.encode(message) : message
-
server.pubsub.broadcast broadcasting, encoded
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActionCable
-
1
module Server
-
# An instance of this configuration object is available via ActionCable.server.config, which allows you to tweak Action Cable configuration
-
# in a Rails config initializer.
-
1
class Configuration
-
1
attr_accessor :logger, :log_tags
-
1
attr_accessor :connection_class, :worker_pool_size
-
1
attr_accessor :disable_request_forgery_protection, :allowed_request_origins, :allow_same_origin_as_host
-
1
attr_accessor :cable, :url, :mount_path
-
-
1
def initialize
-
1
@log_tags = []
-
-
1
@connection_class = -> { ActionCable::Connection::Base }
-
1
@worker_pool_size = 4
-
-
1
@disable_request_forgery_protection = false
-
1
@allow_same_origin_as_host = true
-
end
-
-
# Returns constant of subscription adapter specified in config/cable.yml.
-
# If the adapter cannot be found, this will default to the Redis adapter.
-
# Also makes sure proper dependencies are required.
-
1
def pubsub_adapter
-
adapter = (cable.fetch("adapter") { "redis" })
-
-
# Require the adapter itself and give useful feedback about
-
# 1. Missing adapter gems and
-
# 2. Adapter gems' missing dependencies.
-
path_to_adapter = "action_cable/subscription_adapter/#{adapter}"
-
begin
-
require path_to_adapter
-
rescue LoadError => e
-
# We couldn't require the adapter itself. Raise an exception that
-
# points out config typos and missing gems.
-
if e.path == path_to_adapter
-
# We can assume that a non-builtin adapter was specified, so it's
-
# either misspelled or missing from Gemfile.
-
raise e.class, "Could not load the '#{adapter}' Action Cable pubsub adapter. Ensure that the adapter is spelled correctly in config/cable.yml and that you've added the necessary adapter gem to your Gemfile.", e.backtrace
-
-
# Bubbled up from the adapter require. Prefix the exception message
-
# with some guidance about how to address it and reraise.
-
else
-
raise e.class, "Error loading the '#{adapter}' Action Cable pubsub adapter. Missing a gem it depends on? #{e.message}", e.backtrace
-
end
-
end
-
-
adapter = adapter.camelize
-
adapter = "PostgreSQL" if adapter == "Postgresql"
-
"ActionCable::SubscriptionAdapter::#{adapter}".constantize
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActionCable
-
1
module Server
-
# Collection class for all the connections that have been established on this specific server. Remember, usually you'll run many Action Cable servers, so
-
# you can't use this collection as a full list of all of the connections established against your application. Instead, use RemoteConnections for that.
-
1
module Connections # :nodoc:
-
1
BEAT_INTERVAL = 3
-
-
1
def connections
-
@connections ||= []
-
end
-
-
1
def add_connection(connection)
-
connections << connection
-
end
-
-
1
def remove_connection(connection)
-
connections.delete connection
-
end
-
-
# WebSocket connection implementations differ on when they'll mark a connection as stale. We basically never want a connection to go stale, as you
-
# then can't rely on being able to communicate with the connection. To solve this, a 3 second heartbeat runs on all connections. If the beat fails, we automatically
-
# disconnect.
-
1
def setup_heartbeat_timer
-
@heartbeat_timer ||= event_loop.timer(BEAT_INTERVAL) do
-
event_loop.post { connections.map(&:beat) }
-
end
-
end
-
-
1
def open_connections_statistics
-
connections.map(&:statistics)
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_support/callbacks"
-
1
require "active_support/core_ext/module/attribute_accessors_per_thread"
-
1
require "concurrent"
-
-
1
module ActionCable
-
1
module Server
-
# Worker used by Server.send_async to do connection work in threads.
-
1
class Worker # :nodoc:
-
1
include ActiveSupport::Callbacks
-
-
1
thread_mattr_accessor :connection
-
1
define_callbacks :work
-
1
include ActiveRecordConnectionManagement
-
-
1
attr_reader :executor
-
-
1
def initialize(max_size: 5)
-
@executor = Concurrent::ThreadPoolExecutor.new(
-
min_threads: 1,
-
max_threads: max_size,
-
max_queue: 0,
-
)
-
end
-
-
# Stop processing work: any work that has not already started
-
# running will be discarded from the queue
-
1
def halt
-
@executor.shutdown
-
end
-
-
1
def stopping?
-
@executor.shuttingdown?
-
end
-
-
1
def work(connection)
-
self.connection = connection
-
-
run_callbacks :work do
-
yield
-
end
-
ensure
-
self.connection = nil
-
end
-
-
1
def async_exec(receiver, *args, connection:, &block)
-
async_invoke receiver, :instance_exec, *args, connection: connection, &block
-
end
-
-
1
def async_invoke(receiver, method, *args, connection: receiver, &block)
-
@executor.post do
-
invoke(receiver, method, *args, connection: connection, &block)
-
end
-
end
-
-
1
def invoke(receiver, method, *args, connection:, &block)
-
work(connection) do
-
begin
-
receiver.send method, *args, &block
-
rescue Exception => e
-
logger.error "There was an exception - #{e.class}(#{e.message})"
-
logger.error e.backtrace.join("\n")
-
-
receiver.handle_exception if receiver.respond_to?(:handle_exception)
-
end
-
end
-
end
-
-
1
private
-
-
1
def logger
-
ActionCable.server.logger
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActionCable
-
1
module Server
-
1
class Worker
-
1
module ActiveRecordConnectionManagement
-
1
extend ActiveSupport::Concern
-
-
1
included do
-
1
if defined?(ActiveRecord::Base)
-
1
set_callback :work, :around, :with_database_connections
-
end
-
end
-
-
1
def with_database_connections
-
connection.logger.tag(ActiveRecord::Base.logger) { yield }
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "mail"
-
1
require "action_mailer/collector"
-
1
require "active_support/core_ext/string/inflections"
-
1
require "active_support/core_ext/hash/except"
-
1
require "active_support/core_ext/module/anonymous"
-
-
1
require "action_mailer/log_subscriber"
-
1
require "action_mailer/rescuable"
-
-
1
module ActionMailer
-
# Action Mailer allows you to send email from your application using a mailer model and views.
-
#
-
# = Mailer Models
-
#
-
# To use Action Mailer, you need to create a mailer model.
-
#
-
# $ rails generate mailer Notifier
-
#
-
# The generated model inherits from <tt>ApplicationMailer</tt> which in turn
-
# inherits from <tt>ActionMailer::Base</tt>. A mailer model defines methods
-
# used to generate an email message. In these methods, you can setup variables to be used in
-
# the mailer views, options on the mail itself such as the <tt>:from</tt> address, and attachments.
-
#
-
# class ApplicationMailer < ActionMailer::Base
-
# default from: 'from@example.com'
-
# layout 'mailer'
-
# end
-
#
-
# class NotifierMailer < ApplicationMailer
-
# default from: 'no-reply@example.com',
-
# return_path: 'system@example.com'
-
#
-
# def welcome(recipient)
-
# @account = recipient
-
# mail(to: recipient.email_address_with_name,
-
# bcc: ["bcc@example.com", "Order Watcher <watcher@example.com>"])
-
# end
-
# end
-
#
-
# Within the mailer method, you have access to the following methods:
-
#
-
# * <tt>attachments[]=</tt> - Allows you to add attachments to your email in an intuitive
-
# manner; <tt>attachments['filename.png'] = File.read('path/to/filename.png')</tt>
-
#
-
# * <tt>attachments.inline[]=</tt> - Allows you to add an inline attachment to your email
-
# in the same manner as <tt>attachments[]=</tt>
-
#
-
# * <tt>headers[]=</tt> - Allows you to specify any header field in your email such
-
# as <tt>headers['X-No-Spam'] = 'True'</tt>. Note that declaring a header multiple times
-
# will add many fields of the same name. Read #headers doc for more information.
-
#
-
# * <tt>headers(hash)</tt> - Allows you to specify multiple headers in your email such
-
# as <tt>headers({'X-No-Spam' => 'True', 'In-Reply-To' => '1234@message.id'})</tt>
-
#
-
# * <tt>mail</tt> - Allows you to specify email to be sent.
-
#
-
# The hash passed to the mail method allows you to specify any header that a <tt>Mail::Message</tt>
-
# will accept (any valid email header including optional fields).
-
#
-
# The +mail+ method, if not passed a block, will inspect your views and send all the views with
-
# the same name as the method, so the above action would send the +welcome.text.erb+ view
-
# file as well as the +welcome.html.erb+ view file in a +multipart/alternative+ email.
-
#
-
# If you want to explicitly render only certain templates, pass a block:
-
#
-
# mail(to: user.email) do |format|
-
# format.text
-
# format.html
-
# end
-
#
-
# The block syntax is also useful in providing information specific to a part:
-
#
-
# mail(to: user.email) do |format|
-
# format.text(content_transfer_encoding: "base64")
-
# format.html
-
# end
-
#
-
# Or even to render a special view:
-
#
-
# mail(to: user.email) do |format|
-
# format.text
-
# format.html { render "some_other_template" }
-
# end
-
#
-
# = Mailer views
-
#
-
# Like Action Controller, each mailer class has a corresponding view directory in which each
-
# method of the class looks for a template with its name.
-
#
-
# To define a template to be used with a mailer, create an <tt>.erb</tt> file with the same
-
# name as the method in your mailer model. For example, in the mailer defined above, the template at
-
# <tt>app/views/notifier_mailer/welcome.text.erb</tt> would be used to generate the email.
-
#
-
# Variables defined in the methods of your mailer model are accessible as instance variables in their
-
# corresponding view.
-
#
-
# Emails by default are sent in plain text, so a sample view for our model example might look like this:
-
#
-
# Hi <%= @account.name %>,
-
# Thanks for joining our service! Please check back often.
-
#
-
# You can even use Action View helpers in these views. For example:
-
#
-
# You got a new note!
-
# <%= truncate(@note.body, length: 25) %>
-
#
-
# If you need to access the subject, from or the recipients in the view, you can do that through message object:
-
#
-
# You got a new note from <%= message.from %>!
-
# <%= truncate(@note.body, length: 25) %>
-
#
-
#
-
# = Generating URLs
-
#
-
# URLs can be generated in mailer views using <tt>url_for</tt> or named routes. Unlike controllers from
-
# Action Pack, the mailer instance doesn't have any context about the incoming request, so you'll need
-
# to provide all of the details needed to generate a URL.
-
#
-
# When using <tt>url_for</tt> you'll need to provide the <tt>:host</tt>, <tt>:controller</tt>, and <tt>:action</tt>:
-
#
-
# <%= url_for(host: "example.com", controller: "welcome", action: "greeting") %>
-
#
-
# When using named routes you only need to supply the <tt>:host</tt>:
-
#
-
# <%= users_url(host: "example.com") %>
-
#
-
# You should use the <tt>named_route_url</tt> style (which generates absolute URLs) and avoid using the
-
# <tt>named_route_path</tt> style (which generates relative URLs), since clients reading the mail will
-
# have no concept of a current URL from which to determine a relative path.
-
#
-
# It is also possible to set a default host that will be used in all mailers by setting the <tt>:host</tt>
-
# option as a configuration option in <tt>config/application.rb</tt>:
-
#
-
# config.action_mailer.default_url_options = { host: "example.com" }
-
#
-
# You can also define a <tt>default_url_options</tt> method on individual mailers to override these
-
# default settings per-mailer.
-
#
-
# By default when <tt>config.force_ssl</tt> is +true+, URLs generated for hosts will use the HTTPS protocol.
-
#
-
# = Sending mail
-
#
-
# Once a mailer action and template are defined, you can deliver your message or defer its creation and
-
# delivery for later:
-
#
-
# NotifierMailer.welcome(User.first).deliver_now # sends the email
-
# mail = NotifierMailer.welcome(User.first) # => an ActionMailer::MessageDelivery object
-
# mail.deliver_now # generates and sends the email now
-
#
-
# The <tt>ActionMailer::MessageDelivery</tt> class is a wrapper around a delegate that will call
-
# your method to generate the mail. If you want direct access to the delegator, or <tt>Mail::Message</tt>,
-
# you can call the <tt>message</tt> method on the <tt>ActionMailer::MessageDelivery</tt> object.
-
#
-
# NotifierMailer.welcome(User.first).message # => a Mail::Message object
-
#
-
# Action Mailer is nicely integrated with Active Job so you can generate and send emails in the background
-
# (example: outside of the request-response cycle, so the user doesn't have to wait on it):
-
#
-
# NotifierMailer.welcome(User.first).deliver_later # enqueue the email sending to Active Job
-
#
-
# Note that <tt>deliver_later</tt> will execute your method from the background job.
-
#
-
# You never instantiate your mailer class. Rather, you just call the method you defined on the class itself.
-
# All instance methods are expected to return a message object to be sent.
-
#
-
# = Multipart Emails
-
#
-
# Multipart messages can also be used implicitly because Action Mailer will automatically detect and use
-
# multipart templates, where each template is named after the name of the action, followed by the content
-
# type. Each such detected template will be added to the message, as a separate part.
-
#
-
# For example, if the following templates exist:
-
# * signup_notification.text.erb
-
# * signup_notification.html.erb
-
# * signup_notification.xml.builder
-
# * signup_notification.yml.erb
-
#
-
# Each would be rendered and added as a separate part to the message, with the corresponding content
-
# type. The content type for the entire message is automatically set to <tt>multipart/alternative</tt>,
-
# which indicates that the email contains multiple different representations of the same email
-
# body. The same instance variables defined in the action are passed to all email templates.
-
#
-
# Implicit template rendering is not performed if any attachments or parts have been added to the email.
-
# This means that you'll have to manually add each part to the email and set the content type of the email
-
# to <tt>multipart/alternative</tt>.
-
#
-
# = Attachments
-
#
-
# Sending attachment in emails is easy:
-
#
-
# class NotifierMailer < ApplicationMailer
-
# def welcome(recipient)
-
# attachments['free_book.pdf'] = File.read('path/to/file.pdf')
-
# mail(to: recipient, subject: "New account information")
-
# end
-
# end
-
#
-
# Which will (if it had both a <tt>welcome.text.erb</tt> and <tt>welcome.html.erb</tt>
-
# template in the view directory), send a complete <tt>multipart/mixed</tt> email with two parts,
-
# the first part being a <tt>multipart/alternative</tt> with the text and HTML email parts inside,
-
# and the second being a <tt>application/pdf</tt> with a Base64 encoded copy of the file.pdf book
-
# with the filename +free_book.pdf+.
-
#
-
# If you need to send attachments with no content, you need to create an empty view for it,
-
# or add an empty body parameter like this:
-
#
-
# class NotifierMailer < ApplicationMailer
-
# def welcome(recipient)
-
# attachments['free_book.pdf'] = File.read('path/to/file.pdf')
-
# mail(to: recipient, subject: "New account information", body: "")
-
# end
-
# end
-
#
-
# You can also send attachments with html template, in this case you need to add body, attachments,
-
# and custom content type like this:
-
#
-
# class NotifierMailer < ApplicationMailer
-
# def welcome(recipient)
-
# attachments["free_book.pdf"] = File.read("path/to/file.pdf")
-
# mail(to: recipient,
-
# subject: "New account information",
-
# content_type: "text/html",
-
# body: "<html><body>Hello there</body></html>")
-
# end
-
# end
-
#
-
# = Inline Attachments
-
#
-
# You can also specify that a file should be displayed inline with other HTML. This is useful
-
# if you want to display a corporate logo or a photo.
-
#
-
# class NotifierMailer < ApplicationMailer
-
# def welcome(recipient)
-
# attachments.inline['photo.png'] = File.read('path/to/photo.png')
-
# mail(to: recipient, subject: "Here is what we look like")
-
# end
-
# end
-
#
-
# And then to reference the image in the view, you create a <tt>welcome.html.erb</tt> file and
-
# make a call to +image_tag+ passing in the attachment you want to display and then call
-
# +url+ on the attachment to get the relative content id path for the image source:
-
#
-
# <h1>Please Don't Cringe</h1>
-
#
-
# <%= image_tag attachments['photo.png'].url -%>
-
#
-
# As we are using Action View's +image_tag+ method, you can pass in any other options you want:
-
#
-
# <h1>Please Don't Cringe</h1>
-
#
-
# <%= image_tag attachments['photo.png'].url, alt: 'Our Photo', class: 'photo' -%>
-
#
-
# = Observing and Intercepting Mails
-
#
-
# Action Mailer provides hooks into the Mail observer and interceptor methods. These allow you to
-
# register classes that are called during the mail delivery life cycle.
-
#
-
# An observer class must implement the <tt>:delivered_email(message)</tt> method which will be
-
# called once for every email sent after the email has been sent.
-
#
-
# An interceptor class must implement the <tt>:delivering_email(message)</tt> method which will be
-
# called before the email is sent, allowing you to make modifications to the email before it hits
-
# the delivery agents. Your class should make any needed modifications directly to the passed
-
# in <tt>Mail::Message</tt> instance.
-
#
-
# = Default Hash
-
#
-
# Action Mailer provides some intelligent defaults for your emails, these are usually specified in a
-
# default method inside the class definition:
-
#
-
# class NotifierMailer < ApplicationMailer
-
# default sender: 'system@example.com'
-
# end
-
#
-
# You can pass in any header value that a <tt>Mail::Message</tt> accepts. Out of the box,
-
# <tt>ActionMailer::Base</tt> sets the following:
-
#
-
# * <tt>mime_version: "1.0"</tt>
-
# * <tt>charset: "UTF-8"</tt>
-
# * <tt>content_type: "text/plain"</tt>
-
# * <tt>parts_order: [ "text/plain", "text/enriched", "text/html" ]</tt>
-
#
-
# <tt>parts_order</tt> and <tt>charset</tt> are not actually valid <tt>Mail::Message</tt> header fields,
-
# but Action Mailer translates them appropriately and sets the correct values.
-
#
-
# As you can pass in any header, you need to either quote the header as a string, or pass it in as
-
# an underscored symbol, so the following will work:
-
#
-
# class NotifierMailer < ApplicationMailer
-
# default 'Content-Transfer-Encoding' => '7bit',
-
# content_description: 'This is a description'
-
# end
-
#
-
# Finally, Action Mailer also supports passing <tt>Proc</tt> and <tt>Lambda</tt> objects into the default hash,
-
# so you can define methods that evaluate as the message is being generated:
-
#
-
# class NotifierMailer < ApplicationMailer
-
# default 'X-Special-Header' => Proc.new { my_method }, to: -> { @inviter.email_address }
-
#
-
# private
-
# def my_method
-
# 'some complex call'
-
# end
-
# end
-
#
-
# Note that the proc/lambda is evaluated right at the start of the mail message generation, so if you
-
# set something in the default hash using a proc, and then set the same thing inside of your
-
# mailer method, it will get overwritten by the mailer method.
-
#
-
# It is also possible to set these default options that will be used in all mailers through
-
# the <tt>default_options=</tt> configuration in <tt>config/application.rb</tt>:
-
#
-
# config.action_mailer.default_options = { from: "no-reply@example.org" }
-
#
-
# = Callbacks
-
#
-
# You can specify callbacks using <tt>before_action</tt> and <tt>after_action</tt> for configuring your messages.
-
# This may be useful, for example, when you want to add default inline attachments for all
-
# messages sent out by a certain mailer class:
-
#
-
# class NotifierMailer < ApplicationMailer
-
# before_action :add_inline_attachment!
-
#
-
# def welcome
-
# mail
-
# end
-
#
-
# private
-
# def add_inline_attachment!
-
# attachments.inline["footer.jpg"] = File.read('/path/to/filename.jpg')
-
# end
-
# end
-
#
-
# Callbacks in Action Mailer are implemented using
-
# <tt>AbstractController::Callbacks</tt>, so you can define and configure
-
# callbacks in the same manner that you would use callbacks in classes that
-
# inherit from <tt>ActionController::Base</tt>.
-
#
-
# Note that unless you have a specific reason to do so, you should prefer
-
# using <tt>before_action</tt> rather than <tt>after_action</tt> in your
-
# Action Mailer classes so that headers are parsed properly.
-
#
-
# = Previewing emails
-
#
-
# You can preview your email templates visually by adding a mailer preview file to the
-
# <tt>ActionMailer::Base.preview_path</tt>. Since most emails do something interesting
-
# with database data, you'll need to write some scenarios to load messages with fake data:
-
#
-
# class NotifierMailerPreview < ActionMailer::Preview
-
# def welcome
-
# NotifierMailer.welcome(User.first)
-
# end
-
# end
-
#
-
# Methods must return a <tt>Mail::Message</tt> object which can be generated by calling the mailer
-
# method without the additional <tt>deliver_now</tt> / <tt>deliver_later</tt>. The location of the
-
# mailer previews directory can be configured using the <tt>preview_path</tt> option which has a default
-
# of <tt>test/mailers/previews</tt>:
-
#
-
# config.action_mailer.preview_path = "#{Rails.root}/lib/mailer_previews"
-
#
-
# An overview of all previews is accessible at <tt>http://localhost:3000/rails/mailers</tt>
-
# on a running development server instance.
-
#
-
# Previews can also be intercepted in a similar manner as deliveries can be by registering
-
# a preview interceptor that has a <tt>previewing_email</tt> method:
-
#
-
# class CssInlineStyler
-
# def self.previewing_email(message)
-
# # inline CSS styles
-
# end
-
# end
-
#
-
# config.action_mailer.preview_interceptors :css_inline_styler
-
#
-
# Note that interceptors need to be registered both with <tt>register_interceptor</tt>
-
# and <tt>register_preview_interceptor</tt> if they should operate on both sending and
-
# previewing emails.
-
#
-
# = Configuration options
-
#
-
# These options are specified on the class level, like
-
# <tt>ActionMailer::Base.raise_delivery_errors = true</tt>
-
#
-
# * <tt>default_options</tt> - You can pass this in at a class level as well as within the class itself as
-
# per the above section.
-
#
-
# * <tt>logger</tt> - the logger is used for generating information on the mailing run if available.
-
# Can be set to +nil+ for no logging. Compatible with both Ruby's own +Logger+ and Log4r loggers.
-
#
-
# * <tt>smtp_settings</tt> - Allows detailed configuration for <tt>:smtp</tt> delivery method:
-
# * <tt>:address</tt> - Allows you to use a remote mail server. Just change it from its default
-
# "localhost" setting.
-
# * <tt>:port</tt> - On the off chance that your mail server doesn't run on port 25, you can change it.
-
# * <tt>:domain</tt> - If you need to specify a HELO domain, you can do it here.
-
# * <tt>:user_name</tt> - If your mail server requires authentication, set the username in this setting.
-
# * <tt>:password</tt> - If your mail server requires authentication, set the password in this setting.
-
# * <tt>:authentication</tt> - If your mail server requires authentication, you need to specify the
-
# authentication type here.
-
# This is a symbol and one of <tt>:plain</tt> (will send the password Base64 encoded), <tt>:login</tt> (will
-
# send the password Base64 encoded) or <tt>:cram_md5</tt> (combines a Challenge/Response mechanism to exchange
-
# information and a cryptographic Message Digest 5 algorithm to hash important information)
-
# * <tt>:enable_starttls_auto</tt> - Detects if STARTTLS is enabled in your SMTP server and starts
-
# to use it. Defaults to <tt>true</tt>.
-
# * <tt>:openssl_verify_mode</tt> - When using TLS, you can set how OpenSSL checks the certificate. This is
-
# really useful if you need to validate a self-signed and/or a wildcard certificate. You can use the name
-
# of an OpenSSL verify constant (<tt>'none'</tt> or <tt>'peer'</tt>) or directly the constant
-
# (<tt>OpenSSL::SSL::VERIFY_NONE</tt> or <tt>OpenSSL::SSL::VERIFY_PEER</tt>).
-
# <tt>:ssl/:tls</tt> Enables the SMTP connection to use SMTP/TLS (SMTPS: SMTP over direct TLS connection)
-
#
-
# * <tt>sendmail_settings</tt> - Allows you to override options for the <tt>:sendmail</tt> delivery method.
-
# * <tt>:location</tt> - The location of the sendmail executable. Defaults to <tt>/usr/sbin/sendmail</tt>.
-
# * <tt>:arguments</tt> - The command line arguments. Defaults to <tt>-i</tt> with <tt>-f sender@address</tt>
-
# added automatically before the message is sent.
-
#
-
# * <tt>file_settings</tt> - Allows you to override options for the <tt>:file</tt> delivery method.
-
# * <tt>:location</tt> - The directory into which emails will be written. Defaults to the application
-
# <tt>tmp/mails</tt>.
-
#
-
# * <tt>raise_delivery_errors</tt> - Whether or not errors should be raised if the email fails to be delivered.
-
#
-
# * <tt>delivery_method</tt> - Defines a delivery method. Possible values are <tt>:smtp</tt> (default),
-
# <tt>:sendmail</tt>, <tt>:test</tt>, and <tt>:file</tt>. Or you may provide a custom delivery method
-
# object e.g. +MyOwnDeliveryMethodClass+. See the Mail gem documentation on the interface you need to
-
# implement for a custom delivery agent.
-
#
-
# * <tt>perform_deliveries</tt> - Determines whether emails are actually sent from Action Mailer when you
-
# call <tt>.deliver</tt> on an email message or on an Action Mailer method. This is on by default but can
-
# be turned off to aid in functional testing.
-
#
-
# * <tt>deliveries</tt> - Keeps an array of all the emails sent out through the Action Mailer with
-
# <tt>delivery_method :test</tt>. Most useful for unit and functional testing.
-
#
-
# * <tt>deliver_later_queue_name</tt> - The name of the queue used with <tt>deliver_later</tt>. Defaults to +mailers+.
-
1
class Base < AbstractController::Base
-
1
include DeliveryMethods
-
1
include Rescuable
-
1
include Parameterized
-
1
include Previews
-
-
1
abstract!
-
-
1
include AbstractController::Rendering
-
-
1
include AbstractController::Logger
-
1
include AbstractController::Helpers
-
1
include AbstractController::Translation
-
1
include AbstractController::AssetPaths
-
1
include AbstractController::Callbacks
-
1
include AbstractController::Caching
-
-
1
include ActionView::Layouts
-
-
1
PROTECTED_IVARS = AbstractController::Rendering::DEFAULT_PROTECTED_INSTANCE_VARIABLES + [:@_action_has_layout]
-
-
1
def _protected_ivars # :nodoc:
-
PROTECTED_IVARS
-
end
-
-
1
helper ActionMailer::MailHelper
-
-
1
class_attribute :delivery_job, default: ::ActionMailer::DeliveryJob
-
1
class_attribute :default_params, default: {
-
mime_version: "1.0",
-
charset: "UTF-8",
-
content_type: "text/plain",
-
parts_order: [ "text/plain", "text/enriched", "text/html" ]
-
}.freeze
-
-
1
class << self
-
# Register one or more Observers which will be notified when mail is delivered.
-
1
def register_observers(*observers)
-
1
observers.flatten.compact.each { |observer| register_observer(observer) }
-
end
-
-
# Register one or more Interceptors which will be called before mail is sent.
-
1
def register_interceptors(*interceptors)
-
1
interceptors.flatten.compact.each { |interceptor| register_interceptor(interceptor) }
-
end
-
-
# Register an Observer which will be notified when mail is delivered.
-
# Either a class, string or symbol can be passed in as the Observer.
-
# If a string or symbol is passed in it will be camelized and constantized.
-
1
def register_observer(observer)
-
Mail.register_observer(observer_class_for(observer))
-
end
-
-
# Register an Interceptor which will be called before mail is sent.
-
# Either a class, string or symbol can be passed in as the Interceptor.
-
# If a string or symbol is passed in it will be camelized and constantized.
-
1
def register_interceptor(interceptor)
-
Mail.register_interceptor(observer_class_for(interceptor))
-
end
-
-
1
def observer_class_for(value) # :nodoc:
-
case value
-
when String, Symbol
-
value.to_s.camelize.constantize
-
else
-
value
-
end
-
end
-
1
private :observer_class_for
-
-
# Returns the name of the current mailer. This method is also being used as a path for a view lookup.
-
# If this is an anonymous mailer, this method will return +anonymous+ instead.
-
1
def mailer_name
-
@mailer_name ||= anonymous? ? "anonymous" : name.underscore
-
end
-
# Allows to set the name of current mailer.
-
1
attr_writer :mailer_name
-
1
alias :controller_path :mailer_name
-
-
# Sets the defaults through app configuration:
-
#
-
# config.action_mailer.default(from: "no-reply@example.org")
-
#
-
# Aliased by ::default_options=
-
1
def default(value = nil)
-
self.default_params = default_params.merge(value).freeze if value
-
default_params
-
end
-
# Allows to set defaults through app configuration:
-
#
-
# config.action_mailer.default_options = { from: "no-reply@example.org" }
-
1
alias :default_options= :default
-
-
# Receives a raw email, parses it into an email object, decodes it,
-
# instantiates a new mailer, and passes the email object to the mailer
-
# object's +receive+ method.
-
#
-
# If you want your mailer to be able to process incoming messages, you'll
-
# need to implement a +receive+ method that accepts the raw email string
-
# as a parameter:
-
#
-
# class MyMailer < ActionMailer::Base
-
# def receive(mail)
-
# # ...
-
# end
-
# end
-
1
def receive(raw_mail)
-
ActiveSupport::Notifications.instrument("receive.action_mailer") do |payload|
-
mail = Mail.new(raw_mail)
-
set_payload_for_mail(payload, mail)
-
new.receive(mail)
-
end
-
end
-
-
# Wraps an email delivery inside of <tt>ActiveSupport::Notifications</tt> instrumentation.
-
#
-
# This method is actually called by the <tt>Mail::Message</tt> object itself
-
# through a callback when you call <tt>:deliver</tt> on the <tt>Mail::Message</tt>,
-
# calling +deliver_mail+ directly and passing a <tt>Mail::Message</tt> will do
-
# nothing except tell the logger you sent the email.
-
1
def deliver_mail(mail) #:nodoc:
-
ActiveSupport::Notifications.instrument("deliver.action_mailer") do |payload|
-
set_payload_for_mail(payload, mail)
-
yield # Let Mail do the delivery actions
-
end
-
end
-
-
1
private
-
-
1
def set_payload_for_mail(payload, mail)
-
payload[:mailer] = name
-
payload[:message_id] = mail.message_id
-
payload[:subject] = mail.subject
-
payload[:to] = mail.to
-
payload[:from] = mail.from
-
payload[:bcc] = mail.bcc if mail.bcc.present?
-
payload[:cc] = mail.cc if mail.cc.present?
-
payload[:date] = mail.date
-
payload[:mail] = mail.encoded
-
end
-
-
1
def method_missing(method_name, *args)
-
if action_methods.include?(method_name.to_s)
-
MessageDelivery.new(self, method_name, *args)
-
else
-
super
-
end
-
end
-
-
1
def respond_to_missing?(method, include_all = false)
-
1
action_methods.include?(method.to_s) || super
-
end
-
end
-
-
1
attr_internal :message
-
-
1
def initialize
-
super()
-
@_mail_was_called = false
-
@_message = Mail.new
-
end
-
-
1
def process(method_name, *args) #:nodoc:
-
payload = {
-
mailer: self.class.name,
-
action: method_name,
-
args: args
-
}
-
-
ActiveSupport::Notifications.instrument("process.action_mailer", payload) do
-
super
-
@_message = NullMail.new unless @_mail_was_called
-
end
-
end
-
-
1
class NullMail #:nodoc:
-
1
def body; "" end
-
1
def header; {} end
-
-
1
def respond_to?(string, include_all = false)
-
true
-
end
-
-
1
def method_missing(*args)
-
nil
-
end
-
end
-
-
# Returns the name of the mailer object.
-
1
def mailer_name
-
self.class.mailer_name
-
end
-
-
# Allows you to pass random and unusual headers to the new <tt>Mail::Message</tt>
-
# object which will add them to itself.
-
#
-
# headers['X-Special-Domain-Specific-Header'] = "SecretValue"
-
#
-
# You can also pass a hash into headers of header field names and values,
-
# which will then be set on the <tt>Mail::Message</tt> object:
-
#
-
# headers 'X-Special-Domain-Specific-Header' => "SecretValue",
-
# 'In-Reply-To' => incoming.message_id
-
#
-
# The resulting <tt>Mail::Message</tt> will have the following in its header:
-
#
-
# X-Special-Domain-Specific-Header: SecretValue
-
#
-
# Note about replacing already defined headers:
-
#
-
# * +subject+
-
# * +sender+
-
# * +from+
-
# * +to+
-
# * +cc+
-
# * +bcc+
-
# * +reply-to+
-
# * +orig-date+
-
# * +message-id+
-
# * +references+
-
#
-
# Fields can only appear once in email headers while other fields such as
-
# <tt>X-Anything</tt> can appear multiple times.
-
#
-
# If you want to replace any header which already exists, first set it to
-
# +nil+ in order to reset the value otherwise another field will be added
-
# for the same header.
-
1
def headers(args = nil)
-
if args
-
@_message.headers(args)
-
else
-
@_message
-
end
-
end
-
-
# Allows you to add attachments to an email, like so:
-
#
-
# mail.attachments['filename.jpg'] = File.read('/path/to/filename.jpg')
-
#
-
# If you do this, then Mail will take the file name and work out the mime type.
-
# It will also set the Content-Type, Content-Disposition, Content-Transfer-Encoding
-
# and encode the contents of the attachment in Base64.
-
#
-
# You can also specify overrides if you want by passing a hash instead of a string:
-
#
-
# mail.attachments['filename.jpg'] = {mime_type: 'application/gzip',
-
# content: File.read('/path/to/filename.jpg')}
-
#
-
# If you want to use encoding other than Base64 then you will need to pass encoding
-
# type along with the pre-encoded content as Mail doesn't know how to decode the
-
# data:
-
#
-
# file_content = SpecialEncode(File.read('/path/to/filename.jpg'))
-
# mail.attachments['filename.jpg'] = {mime_type: 'application/gzip',
-
# encoding: 'SpecialEncoding',
-
# content: file_content }
-
#
-
# You can also search for specific attachments:
-
#
-
# # By Filename
-
# mail.attachments['filename.jpg'] # => Mail::Part object or nil
-
#
-
# # or by index
-
# mail.attachments[0] # => Mail::Part (first attachment)
-
#
-
1
def attachments
-
if @_mail_was_called
-
LateAttachmentsProxy.new(@_message.attachments)
-
else
-
@_message.attachments
-
end
-
end
-
-
1
class LateAttachmentsProxy < SimpleDelegator
-
1
def inline; _raise_error end
-
1
def []=(_name, _content); _raise_error end
-
-
1
private
-
1
def _raise_error
-
raise RuntimeError, "Can't add attachments after `mail` was called.\n" \
-
"Make sure to use `attachments[]=` before calling `mail`."
-
end
-
end
-
-
# The main method that creates the message and renders the email templates. There are
-
# two ways to call this method, with a block, or without a block.
-
#
-
# It accepts a headers hash. This hash allows you to specify
-
# the most used headers in an email message, these are:
-
#
-
# * +:subject+ - The subject of the message, if this is omitted, Action Mailer will
-
# ask the Rails I18n class for a translated +:subject+ in the scope of
-
# <tt>[mailer_scope, action_name]</tt> or if this is missing, will translate the
-
# humanized version of the +action_name+
-
# * +:to+ - Who the message is destined for, can be a string of addresses, or an array
-
# of addresses.
-
# * +:from+ - Who the message is from
-
# * +:cc+ - Who you would like to Carbon-Copy on this email, can be a string of addresses,
-
# or an array of addresses.
-
# * +:bcc+ - Who you would like to Blind-Carbon-Copy on this email, can be a string of
-
# addresses, or an array of addresses.
-
# * +:reply_to+ - Who to set the Reply-To header of the email to.
-
# * +:date+ - The date to say the email was sent on.
-
#
-
# You can set default values for any of the above headers (except +:date+)
-
# by using the ::default class method:
-
#
-
# class Notifier < ActionMailer::Base
-
# default from: 'no-reply@test.lindsaar.net',
-
# bcc: 'email_logger@test.lindsaar.net',
-
# reply_to: 'bounces@test.lindsaar.net'
-
# end
-
#
-
# If you need other headers not listed above, you can either pass them in
-
# as part of the headers hash or use the <tt>headers['name'] = value</tt>
-
# method.
-
#
-
# When a +:return_path+ is specified as header, that value will be used as
-
# the 'envelope from' address for the Mail message. Setting this is useful
-
# when you want delivery notifications sent to a different address than the
-
# one in +:from+. Mail will actually use the +:return_path+ in preference
-
# to the +:sender+ in preference to the +:from+ field for the 'envelope
-
# from' value.
-
#
-
# If you do not pass a block to the +mail+ method, it will find all
-
# templates in the view paths using by default the mailer name and the
-
# method name that it is being called from, it will then create parts for
-
# each of these templates intelligently, making educated guesses on correct
-
# content type and sequence, and return a fully prepared <tt>Mail::Message</tt>
-
# ready to call <tt>:deliver</tt> on to send.
-
#
-
# For example:
-
#
-
# class Notifier < ActionMailer::Base
-
# default from: 'no-reply@test.lindsaar.net'
-
#
-
# def welcome
-
# mail(to: 'mikel@test.lindsaar.net')
-
# end
-
# end
-
#
-
# Will look for all templates at "app/views/notifier" with name "welcome".
-
# If no welcome template exists, it will raise an ActionView::MissingTemplate error.
-
#
-
# However, those can be customized:
-
#
-
# mail(template_path: 'notifications', template_name: 'another')
-
#
-
# And now it will look for all templates at "app/views/notifications" with name "another".
-
#
-
# If you do pass a block, you can render specific templates of your choice:
-
#
-
# mail(to: 'mikel@test.lindsaar.net') do |format|
-
# format.text
-
# format.html
-
# end
-
#
-
# You can even render plain text directly without using a template:
-
#
-
# mail(to: 'mikel@test.lindsaar.net') do |format|
-
# format.text { render plain: "Hello Mikel!" }
-
# format.html { render html: "<h1>Hello Mikel!</h1>".html_safe }
-
# end
-
#
-
# Which will render a +multipart/alternative+ email with +text/plain+ and
-
# +text/html+ parts.
-
#
-
# The block syntax also allows you to customize the part headers if desired:
-
#
-
# mail(to: 'mikel@test.lindsaar.net') do |format|
-
# format.text(content_transfer_encoding: "base64")
-
# format.html
-
# end
-
#
-
1
def mail(headers = {}, &block)
-
return message if @_mail_was_called && headers.blank? && !block
-
-
# At the beginning, do not consider class default for content_type
-
content_type = headers[:content_type]
-
-
headers = apply_defaults(headers)
-
-
# Apply charset at the beginning so all fields are properly quoted
-
message.charset = charset = headers[:charset]
-
-
# Set configure delivery behavior
-
wrap_delivery_behavior!(headers[:delivery_method], headers[:delivery_method_options])
-
-
assign_headers_to_message(message, headers)
-
-
# Render the templates and blocks
-
responses = collect_responses(headers, &block)
-
@_mail_was_called = true
-
-
create_parts_from_responses(message, responses)
-
-
# Setup content type, reapply charset and handle parts order
-
message.content_type = set_content_type(message, content_type, headers[:content_type])
-
message.charset = charset
-
-
if message.multipart?
-
message.body.set_sort_order(headers[:parts_order])
-
message.body.sort_parts!
-
end
-
-
message
-
end
-
-
1
private
-
-
# Used by #mail to set the content type of the message.
-
#
-
# It will use the given +user_content_type+, or multipart if the mail
-
# message has any attachments. If the attachments are inline, the content
-
# type will be "multipart/related", otherwise "multipart/mixed".
-
#
-
# If there is no content type passed in via headers, and there are no
-
# attachments, or the message is multipart, then the default content type is
-
# used.
-
1
def set_content_type(m, user_content_type, class_default) # :doc:
-
params = m.content_type_parameters || {}
-
case
-
when user_content_type.present?
-
user_content_type
-
when m.has_attachments?
-
if m.attachments.detect(&:inline?)
-
["multipart", "related", params]
-
else
-
["multipart", "mixed", params]
-
end
-
when m.multipart?
-
["multipart", "alternative", params]
-
else
-
m.content_type || class_default
-
end
-
end
-
-
# Translates the +subject+ using Rails I18n class under <tt>[mailer_scope, action_name]</tt> scope.
-
# If it does not find a translation for the +subject+ under the specified scope it will default to a
-
# humanized version of the <tt>action_name</tt>.
-
# If the subject has interpolations, you can pass them through the +interpolations+ parameter.
-
1
def default_i18n_subject(interpolations = {}) # :doc:
-
mailer_scope = self.class.mailer_name.tr("/", ".")
-
I18n.t(:subject, interpolations.merge(scope: [mailer_scope, action_name], default: action_name.humanize))
-
end
-
-
# Emails do not support relative path links.
-
1
def self.supports_path? # :doc:
-
false
-
end
-
-
1
def apply_defaults(headers)
-
default_values = self.class.default.map do |key, value|
-
[
-
key,
-
compute_default(value)
-
]
-
end.to_h
-
-
headers_with_defaults = headers.reverse_merge(default_values)
-
headers_with_defaults[:subject] ||= default_i18n_subject
-
headers_with_defaults
-
end
-
-
1
def compute_default(value)
-
return value unless value.is_a?(Proc)
-
-
if value.arity == 1
-
instance_exec(self, &value)
-
else
-
instance_exec(&value)
-
end
-
end
-
-
1
def assign_headers_to_message(message, headers)
-
assignable = headers.except(:parts_order, :content_type, :body, :template_name,
-
:template_path, :delivery_method, :delivery_method_options)
-
assignable.each { |k, v| message[k] = v }
-
end
-
-
1
def collect_responses(headers)
-
if block_given?
-
collector = ActionMailer::Collector.new(lookup_context) { render(action_name) }
-
yield(collector)
-
collector.responses
-
elsif headers[:body]
-
collect_responses_from_text(headers)
-
else
-
collect_responses_from_templates(headers)
-
end
-
end
-
-
1
def collect_responses_from_text(headers)
-
[{
-
body: headers.delete(:body),
-
content_type: headers[:content_type] || "text/plain"
-
}]
-
end
-
-
1
def collect_responses_from_templates(headers)
-
templates_path = headers[:template_path] || self.class.mailer_name
-
templates_name = headers[:template_name] || action_name
-
-
each_template(Array(templates_path), templates_name).map do |template|
-
self.formats = template.formats
-
{
-
body: render(template: template),
-
content_type: template.type.to_s
-
}
-
end
-
end
-
-
1
def each_template(paths, name, &block)
-
templates = lookup_context.find_all(name, paths)
-
if templates.empty?
-
raise ActionView::MissingTemplate.new(paths, name, paths, false, "mailer")
-
else
-
templates.uniq(&:formats).each(&block)
-
end
-
end
-
-
1
def create_parts_from_responses(m, responses)
-
if responses.size == 1 && !m.has_attachments?
-
responses[0].each { |k, v| m[k] = v }
-
elsif responses.size > 1 && m.has_attachments?
-
container = Mail::Part.new
-
container.content_type = "multipart/alternative"
-
responses.each { |r| insert_part(container, r, m.charset) }
-
m.add_part(container)
-
else
-
responses.each { |r| insert_part(m, r, m.charset) }
-
end
-
end
-
-
1
def insert_part(container, response, charset)
-
response[:charset] ||= charset
-
part = Mail::Part.new(response)
-
container.add_part(part)
-
end
-
-
# This and #instrument_name is for caching instrument
-
1
def instrument_payload(key)
-
{
-
mailer: mailer_name,
-
key: key
-
}
-
end
-
-
1
def instrument_name
-
"action_mailer".freeze
-
end
-
-
1
ActiveSupport.run_load_hooks(:action_mailer, self)
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "abstract_controller/collector"
-
1
require "active_support/core_ext/hash/reverse_merge"
-
1
require "active_support/core_ext/array/extract_options"
-
-
1
module ActionMailer
-
1
class Collector
-
1
include AbstractController::Collector
-
1
attr_reader :responses
-
-
1
def initialize(context, &block)
-
@context = context
-
@responses = []
-
@default_render = block
-
end
-
-
1
def any(*args, &block)
-
options = args.extract_options!
-
raise ArgumentError, "You have to supply at least one format" if args.empty?
-
args.each { |type| send(type, options.dup, &block) }
-
end
-
1
alias :all :any
-
-
1
def custom(mime, options = {})
-
options.reverse_merge!(content_type: mime.to_s)
-
@context.formats = [mime.to_sym]
-
options[:body] = block_given? ? yield : @default_render.call
-
@responses << options
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_job"
-
-
1
module ActionMailer
-
# The <tt>ActionMailer::DeliveryJob</tt> class is used when you
-
# want to send emails outside of the request-response cycle.
-
#
-
# Exceptions are rescued and handled by the mailer class.
-
1
class DeliveryJob < ActiveJob::Base # :nodoc:
-
1
queue_as { ActionMailer::Base.deliver_later_queue_name }
-
-
1
rescue_from StandardError, with: :handle_exception_with_mailer_class
-
-
1
def perform(mailer, mail_method, delivery_method, *args) #:nodoc:
-
mailer.constantize.public_send(mail_method, *args).send(delivery_method)
-
end
-
-
1
private
-
# "Deserialize" the mailer class name by hand in case another argument
-
# (like a Global ID reference) raised DeserializationError.
-
1
def mailer_class
-
if mailer = Array(@serialized_arguments).first || Array(arguments).first
-
mailer.constantize
-
end
-
end
-
-
1
def handle_exception_with_mailer_class(exception)
-
if klass = mailer_class
-
klass.handle_exception exception
-
else
-
raise exception
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "tmpdir"
-
-
1
module ActionMailer
-
# This module handles everything related to mail delivery, from registering
-
# new delivery methods to configuring the mail object to be sent.
-
1
module DeliveryMethods
-
1
extend ActiveSupport::Concern
-
-
1
included do
-
# Do not make this inheritable, because we always want it to propagate
-
1
cattr_accessor :raise_delivery_errors, default: true
-
1
cattr_accessor :perform_deliveries, default: true
-
1
cattr_accessor :deliver_later_queue_name, default: :mailers
-
-
1
class_attribute :delivery_methods, default: {}.freeze
-
1
class_attribute :delivery_method, default: :smtp
-
-
1
add_delivery_method :smtp, Mail::SMTP,
-
address: "localhost",
-
port: 25,
-
domain: "localhost.localdomain",
-
user_name: nil,
-
password: nil,
-
authentication: nil,
-
enable_starttls_auto: true
-
-
2
add_delivery_method :file, Mail::FileDelivery,
-
1
location: defined?(Rails.root) ? "#{Rails.root}/tmp/mails" : "#{Dir.tmpdir}/mails"
-
-
1
add_delivery_method :sendmail, Mail::Sendmail,
-
location: "/usr/sbin/sendmail",
-
arguments: "-i"
-
-
1
add_delivery_method :test, Mail::TestMailer
-
end
-
-
# Helpers for creating and wrapping delivery behavior, used by DeliveryMethods.
-
1
module ClassMethods
-
# Provides a list of emails that have been delivered by Mail::TestMailer
-
1
delegate :deliveries, :deliveries=, to: Mail::TestMailer
-
-
# Adds a new delivery method through the given class using the given
-
# symbol as alias and the default options supplied.
-
#
-
# add_delivery_method :sendmail, Mail::Sendmail,
-
# location: '/usr/sbin/sendmail',
-
# arguments: '-i'
-
1
def add_delivery_method(symbol, klass, default_options = {})
-
4
class_attribute(:"#{symbol}_settings") unless respond_to?(:"#{symbol}_settings")
-
4
send(:"#{symbol}_settings=", default_options)
-
4
self.delivery_methods = delivery_methods.merge(symbol.to_sym => klass).freeze
-
end
-
-
1
def wrap_delivery_behavior(mail, method = nil, options = nil) # :nodoc:
-
method ||= delivery_method
-
mail.delivery_handler = self
-
-
case method
-
when NilClass
-
raise "Delivery method cannot be nil"
-
when Symbol
-
if klass = delivery_methods[method]
-
mail.delivery_method(klass, (send(:"#{method}_settings") || {}).merge(options || {}))
-
else
-
raise "Invalid delivery method #{method.inspect}"
-
end
-
else
-
mail.delivery_method(method)
-
end
-
-
mail.perform_deliveries = perform_deliveries
-
mail.raise_delivery_errors = raise_delivery_errors
-
end
-
end
-
-
1
def wrap_delivery_behavior!(*args) # :nodoc:
-
self.class.wrap_delivery_behavior(message, *args)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "base64"
-
-
1
module ActionMailer
-
# Implements a mailer preview interceptor that converts image tag src attributes
-
# that use inline cid: style URLs to data: style URLs so that they are visible
-
# when previewing an HTML email in a web browser.
-
#
-
# This interceptor is enabled by default. To disable it, delete it from the
-
# <tt>ActionMailer::Base.preview_interceptors</tt> array:
-
#
-
# ActionMailer::Base.preview_interceptors.delete(ActionMailer::InlinePreviewInterceptor)
-
#
-
1
class InlinePreviewInterceptor
-
1
PATTERN = /src=(?:"cid:[^"]+"|'cid:[^']+')/i
-
-
1
include Base64
-
-
1
def self.previewing_email(message) #:nodoc:
-
new(message).transform!
-
end
-
-
1
def initialize(message) #:nodoc:
-
@message = message
-
end
-
-
1
def transform! #:nodoc:
-
return message if html_part.blank?
-
-
html_part.body = html_part.decoded.gsub(PATTERN) do |match|
-
if part = find_part(match[9..-2])
-
%[src="#{data_url(part)}"]
-
else
-
match
-
end
-
end
-
-
message
-
end
-
-
1
private
-
1
def message
-
@message
-
end
-
-
1
def html_part
-
@html_part ||= message.html_part
-
end
-
-
1
def data_url(part)
-
"data:#{part.mime_type};base64,#{strict_encode64(part.body.raw_source)}"
-
end
-
-
1
def find_part(cid)
-
message.all_parts.find { |p| p.attachment? && p.cid == cid }
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_support/log_subscriber"
-
-
1
module ActionMailer
-
# Implements the ActiveSupport::LogSubscriber for logging notifications when
-
# email is delivered or received.
-
1
class LogSubscriber < ActiveSupport::LogSubscriber
-
# An email was delivered.
-
1
def deliver(event)
-
info do
-
recipients = Array(event.payload[:to]).join(", ")
-
"Sent mail to #{recipients} (#{event.duration.round(1)}ms)"
-
end
-
-
debug { event.payload[:mail] }
-
end
-
-
# An email was received.
-
1
def receive(event)
-
info { "Received mail (#{event.duration.round(1)}ms)" }
-
debug { event.payload[:mail] }
-
end
-
-
# An email was generated.
-
1
def process(event)
-
debug do
-
mailer = event.payload[:mailer]
-
action = event.payload[:action]
-
"#{mailer}##{action}: processed outbound mail in #{event.duration.round(1)}ms"
-
end
-
end
-
-
# Use the logger configured for ActionMailer::Base.
-
1
def logger
-
ActionMailer::Base.logger
-
end
-
end
-
end
-
-
1
ActionMailer::LogSubscriber.attach_to :action_mailer
-
# frozen_string_literal: true
-
-
1
module ActionMailer
-
# Provides helper methods for ActionMailer::Base that can be used for easily
-
# formatting messages, accessing mailer or message instances, and the
-
# attachments list.
-
1
module MailHelper
-
# Take the text and format it, indented two spaces for each line, and
-
# wrapped at 72 columns:
-
#
-
# text = <<-TEXT
-
# This is
-
# the paragraph.
-
#
-
# * item1 * item2
-
# TEXT
-
#
-
# block_format text
-
# # => " This is the paragraph.\n\n * item1\n * item2\n"
-
1
def block_format(text)
-
formatted = text.split(/\n\r?\n/).collect { |paragraph|
-
format_paragraph(paragraph)
-
}.join("\n\n")
-
-
# Make list points stand on their own line
-
formatted.gsub!(/[ ]*([*]+) ([^*]*)/) { " #{$1} #{$2.strip}\n" }
-
formatted.gsub!(/[ ]*([#]+) ([^#]*)/) { " #{$1} #{$2.strip}\n" }
-
-
formatted
-
end
-
-
# Access the mailer instance.
-
1
def mailer
-
@_controller
-
end
-
-
# Access the message instance.
-
1
def message
-
@_message
-
end
-
-
# Access the message attachments list.
-
1
def attachments
-
mailer.attachments
-
end
-
-
# Returns +text+ wrapped at +len+ columns and indented +indent+ spaces.
-
# By default column length +len+ equals 72 characters and indent
-
# +indent+ equal two spaces.
-
#
-
# my_text = 'Here is a sample text with more than 40 characters'
-
#
-
# format_paragraph(my_text, 25, 4)
-
# # => " Here is a sample text with\n more than 40 characters"
-
1
def format_paragraph(text, len = 72, indent = 2)
-
sentences = [[]]
-
-
text.split.each do |word|
-
if sentences.first.present? && (sentences.last + [word]).join(" ").length > len
-
sentences << [word]
-
else
-
sentences.last << word
-
end
-
end
-
-
indentation = " " * indent
-
sentences.map! { |sentence|
-
"#{indentation}#{sentence.join(' ')}"
-
}.join "\n"
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "delegate"
-
-
1
module ActionMailer
-
# The <tt>ActionMailer::MessageDelivery</tt> class is used by
-
# ActionMailer::Base when creating a new mailer.
-
# <tt>MessageDelivery</tt> is a wrapper (+Delegator+ subclass) around a lazy
-
# created <tt>Mail::Message</tt>. You can get direct access to the
-
# <tt>Mail::Message</tt>, deliver the email or schedule the email to be sent
-
# through Active Job.
-
#
-
# Notifier.welcome(User.first) # an ActionMailer::MessageDelivery object
-
# Notifier.welcome(User.first).deliver_now # sends the email
-
# Notifier.welcome(User.first).deliver_later # enqueue email delivery as a job through Active Job
-
# Notifier.welcome(User.first).message # a Mail::Message object
-
1
class MessageDelivery < Delegator
-
1
def initialize(mailer_class, action, *args) #:nodoc:
-
@mailer_class, @action, @args = mailer_class, action, args
-
-
# The mail is only processed if we try to call any methods on it.
-
# Typical usage will leave it unloaded and call deliver_later.
-
@processed_mailer = nil
-
@mail_message = nil
-
end
-
-
# Method calls are delegated to the Mail::Message that's ready to deliver.
-
1
def __getobj__ #:nodoc:
-
@mail_message ||= processed_mailer.message
-
end
-
-
# Unused except for delegator internals (dup, marshaling).
-
1
def __setobj__(mail_message) #:nodoc:
-
@mail_message = mail_message
-
end
-
-
# Returns the resulting Mail::Message
-
1
def message
-
__getobj__
-
end
-
-
# Was the delegate loaded, causing the mailer action to be processed?
-
1
def processed?
-
@processed_mailer || @mail_message
-
end
-
-
# Enqueues the email to be delivered through Active Job. When the
-
# job runs it will send the email using +deliver_now!+. That means
-
# that the message will be sent bypassing checking +perform_deliveries+
-
# and +raise_delivery_errors+, so use with caution.
-
#
-
# Notifier.welcome(User.first).deliver_later!
-
# Notifier.welcome(User.first).deliver_later!(wait: 1.hour)
-
# Notifier.welcome(User.first).deliver_later!(wait_until: 10.hours.from_now)
-
#
-
# Options:
-
#
-
# * <tt>:wait</tt> - Enqueue the email to be delivered with a delay
-
# * <tt>:wait_until</tt> - Enqueue the email to be delivered at (after) a specific date / time
-
# * <tt>:queue</tt> - Enqueue the email on the specified queue
-
#
-
# By default, the email will be enqueued using <tt>ActionMailer::DeliveryJob</tt>. Each
-
# <tt>ActionMailer::Base</tt> class can specify the job to use by setting the class variable
-
# +delivery_job+.
-
#
-
# class AccountRegistrationMailer < ApplicationMailer
-
# self.delivery_job = RegistrationDeliveryJob
-
# end
-
1
def deliver_later!(options = {})
-
enqueue_delivery :deliver_now!, options
-
end
-
-
# Enqueues the email to be delivered through Active Job. When the
-
# job runs it will send the email using +deliver_now+.
-
#
-
# Notifier.welcome(User.first).deliver_later
-
# Notifier.welcome(User.first).deliver_later(wait: 1.hour)
-
# Notifier.welcome(User.first).deliver_later(wait_until: 10.hours.from_now)
-
#
-
# Options:
-
#
-
# * <tt>:wait</tt> - Enqueue the email to be delivered with a delay.
-
# * <tt>:wait_until</tt> - Enqueue the email to be delivered at (after) a specific date / time.
-
# * <tt>:queue</tt> - Enqueue the email on the specified queue.
-
#
-
# By default, the email will be enqueued using <tt>ActionMailer::DeliveryJob</tt>. Each
-
# <tt>ActionMailer::Base</tt> class can specify the job to use by setting the class variable
-
# +delivery_job+.
-
#
-
# class AccountRegistrationMailer < ApplicationMailer
-
# self.delivery_job = RegistrationDeliveryJob
-
# end
-
1
def deliver_later(options = {})
-
enqueue_delivery :deliver_now, options
-
end
-
-
# Delivers an email without checking +perform_deliveries+ and +raise_delivery_errors+,
-
# so use with caution.
-
#
-
# Notifier.welcome(User.first).deliver_now!
-
#
-
1
def deliver_now!
-
processed_mailer.handle_exceptions do
-
message.deliver!
-
end
-
end
-
-
# Delivers an email:
-
#
-
# Notifier.welcome(User.first).deliver_now
-
#
-
1
def deliver_now
-
processed_mailer.handle_exceptions do
-
message.deliver
-
end
-
end
-
-
1
private
-
# Returns the processed Mailer instance. We keep this instance
-
# on hand so we can delegate exception handling to it.
-
1
def processed_mailer
-
@processed_mailer ||= @mailer_class.new.tap do |mailer|
-
mailer.process @action, *@args
-
end
-
end
-
-
1
def enqueue_delivery(delivery_method, options = {})
-
if processed?
-
::Kernel.raise "You've accessed the message before asking to " \
-
"deliver it later, so you may have made local changes that would " \
-
"be silently lost if we enqueued a job to deliver it. Why? Only " \
-
"the mailer method *arguments* are passed with the delivery job! " \
-
"Do not access the message in any way if you mean to deliver it " \
-
"later. Workarounds: 1. don't touch the message before calling " \
-
"#deliver_later, 2. only touch the message *within your mailer " \
-
"method*, or 3. use a custom Active Job instead of #deliver_later."
-
else
-
args = @mailer_class.name, @action.to_s, delivery_method.to_s, *@args
-
job = @mailer_class.delivery_job
-
job.set(options).perform_later(*args)
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActionMailer
-
# Provides the option to parameterize mailers in order to share instance variable
-
# setup, processing, and common headers.
-
#
-
# Consider this example that does not use parameterization:
-
#
-
# class InvitationsMailer < ApplicationMailer
-
# def account_invitation(inviter, invitee)
-
# @account = inviter.account
-
# @inviter = inviter
-
# @invitee = invitee
-
#
-
# subject = "#{@inviter.name} invited you to their Basecamp (#{@account.name})"
-
#
-
# mail \
-
# subject: subject,
-
# to: invitee.email_address,
-
# from: common_address(inviter),
-
# reply_to: inviter.email_address_with_name
-
# end
-
#
-
# def project_invitation(project, inviter, invitee)
-
# @account = inviter.account
-
# @project = project
-
# @inviter = inviter
-
# @invitee = invitee
-
# @summarizer = ProjectInvitationSummarizer.new(@project.bucket)
-
#
-
# subject = "#{@inviter.name.familiar} added you to a project in Basecamp (#{@account.name})"
-
#
-
# mail \
-
# subject: subject,
-
# to: invitee.email_address,
-
# from: common_address(inviter),
-
# reply_to: inviter.email_address_with_name
-
# end
-
#
-
# def bulk_project_invitation(projects, inviter, invitee)
-
# @account = inviter.account
-
# @projects = projects.sort_by(&:name)
-
# @inviter = inviter
-
# @invitee = invitee
-
#
-
# subject = "#{@inviter.name.familiar} added you to some new stuff in Basecamp (#{@account.name})"
-
#
-
# mail \
-
# subject: subject,
-
# to: invitee.email_address,
-
# from: common_address(inviter),
-
# reply_to: inviter.email_address_with_name
-
# end
-
# end
-
#
-
# InvitationsMailer.account_invitation(person_a, person_b).deliver_later
-
#
-
# Using parameterized mailers, this can be rewritten as:
-
#
-
# class InvitationsMailer < ApplicationMailer
-
# before_action { @inviter, @invitee = params[:inviter], params[:invitee] }
-
# before_action { @account = params[:inviter].account }
-
#
-
# default to: -> { @invitee.email_address },
-
# from: -> { common_address(@inviter) },
-
# reply_to: -> { @inviter.email_address_with_name }
-
#
-
# def account_invitation
-
# mail subject: "#{@inviter.name} invited you to their Basecamp (#{@account.name})"
-
# end
-
#
-
# def project_invitation
-
# @project = params[:project]
-
# @summarizer = ProjectInvitationSummarizer.new(@project.bucket)
-
#
-
# mail subject: "#{@inviter.name.familiar} added you to a project in Basecamp (#{@account.name})"
-
# end
-
#
-
# def bulk_project_invitation
-
# @projects = params[:projects].sort_by(&:name)
-
#
-
# mail subject: "#{@inviter.name.familiar} added you to some new stuff in Basecamp (#{@account.name})"
-
# end
-
# end
-
#
-
# InvitationsMailer.with(inviter: person_a, invitee: person_b).account_invitation.deliver_later
-
1
module Parameterized
-
1
extend ActiveSupport::Concern
-
-
1
included do
-
1
attr_accessor :params
-
end
-
-
1
module ClassMethods
-
# Provide the parameters to the mailer in order to use them in the instance methods and callbacks.
-
#
-
# InvitationsMailer.with(inviter: person_a, invitee: person_b).account_invitation.deliver_later
-
#
-
# See Parameterized documentation for full example.
-
1
def with(params)
-
ActionMailer::Parameterized::Mailer.new(self, params)
-
end
-
end
-
-
1
class Mailer # :nodoc:
-
1
def initialize(mailer, params)
-
@mailer, @params = mailer, params
-
end
-
-
1
private
-
1
def method_missing(method_name, *args)
-
if @mailer.action_methods.include?(method_name.to_s)
-
ActionMailer::Parameterized::MessageDelivery.new(@mailer, method_name, @params, *args)
-
else
-
super
-
end
-
end
-
-
1
def respond_to_missing?(method, include_all = false)
-
@mailer.respond_to?(method, include_all)
-
end
-
end
-
-
1
class MessageDelivery < ActionMailer::MessageDelivery # :nodoc:
-
1
def initialize(mailer_class, action, params, *args)
-
super(mailer_class, action, *args)
-
@params = params
-
end
-
-
1
private
-
1
def processed_mailer
-
@processed_mailer ||= @mailer_class.new.tap do |mailer|
-
mailer.params = @params
-
mailer.process @action, *@args
-
end
-
end
-
-
1
def enqueue_delivery(delivery_method, options = {})
-
if processed?
-
super
-
else
-
args = @mailer_class.name, @action.to_s, delivery_method.to_s, @params, *@args
-
ActionMailer::Parameterized::DeliveryJob.set(options).perform_later(*args)
-
end
-
end
-
end
-
-
1
class DeliveryJob < ActionMailer::DeliveryJob # :nodoc:
-
1
def perform(mailer, mail_method, delivery_method, params, *args)
-
mailer.constantize.with(params).public_send(mail_method, *args).send(delivery_method)
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_support/descendants_tracker"
-
-
1
module ActionMailer
-
1
module Previews #:nodoc:
-
1
extend ActiveSupport::Concern
-
-
1
included do
-
# Set the location of mailer previews through app configuration:
-
#
-
# config.action_mailer.preview_path = "#{Rails.root}/lib/mailer_previews"
-
#
-
1
mattr_accessor :preview_path, instance_writer: false
-
-
# Enable or disable mailer previews through app configuration:
-
#
-
# config.action_mailer.show_previews = true
-
#
-
# Defaults to +true+ for development environment
-
#
-
1
mattr_accessor :show_previews, instance_writer: false
-
-
# :nodoc:
-
1
mattr_accessor :preview_interceptors, instance_writer: false, default: [ActionMailer::InlinePreviewInterceptor]
-
end
-
-
1
module ClassMethods
-
# Register one or more Interceptors which will be called before mail is previewed.
-
1
def register_preview_interceptors(*interceptors)
-
1
interceptors.flatten.compact.each { |interceptor| register_preview_interceptor(interceptor) }
-
end
-
-
# Register an Interceptor which will be called before mail is previewed.
-
# Either a class or a string can be passed in as the Interceptor. If a
-
# string is passed in it will be constantized.
-
1
def register_preview_interceptor(interceptor)
-
preview_interceptor = \
-
case interceptor
-
when String, Symbol
-
interceptor.to_s.camelize.constantize
-
else
-
interceptor
-
end
-
-
unless preview_interceptors.include?(preview_interceptor)
-
preview_interceptors << preview_interceptor
-
end
-
end
-
end
-
end
-
-
1
class Preview
-
1
extend ActiveSupport::DescendantsTracker
-
-
1
attr_reader :params
-
-
1
def initialize(params = {})
-
@params = params
-
end
-
-
1
class << self
-
# Returns all mailer preview classes.
-
1
def all
-
load_previews if descendants.empty?
-
descendants
-
end
-
-
# Returns the mail object for the given email name. The registered preview
-
# interceptors will be informed so that they can transform the message
-
# as they would if the mail was actually being delivered.
-
1
def call(email, params = {})
-
preview = new(params)
-
message = preview.public_send(email)
-
inform_preview_interceptors(message)
-
message
-
end
-
-
# Returns all of the available email previews.
-
1
def emails
-
public_instance_methods(false).map(&:to_s).sort
-
end
-
-
# Returns +true+ if the email exists.
-
1
def email_exists?(email)
-
emails.include?(email)
-
end
-
-
# Returns +true+ if the preview exists.
-
1
def exists?(preview)
-
all.any? { |p| p.preview_name == preview }
-
end
-
-
# Find a mailer preview by its underscored class name.
-
1
def find(preview)
-
all.find { |p| p.preview_name == preview }
-
end
-
-
# Returns the underscored name of the mailer preview without the suffix.
-
1
def preview_name
-
name.sub(/Preview$/, "").underscore
-
end
-
-
1
private
-
1
def load_previews
-
if preview_path
-
Dir["#{preview_path}/**/*_preview.rb"].sort.each { |file| require_dependency file }
-
end
-
end
-
-
1
def preview_path
-
Base.preview_path
-
end
-
-
1
def show_previews
-
Base.show_previews
-
end
-
-
1
def inform_preview_interceptors(message)
-
Base.preview_interceptors.each do |interceptor|
-
interceptor.previewing_email(message)
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActionMailer #:nodoc:
-
# Provides +rescue_from+ for mailers. Wraps mailer action processing,
-
# mail job processing, and mail delivery.
-
1
module Rescuable
-
1
extend ActiveSupport::Concern
-
1
include ActiveSupport::Rescuable
-
-
1
class_methods do
-
1
def handle_exception(exception) #:nodoc:
-
rescue_with_handler(exception) || raise(exception)
-
end
-
end
-
-
1
def handle_exceptions #:nodoc:
-
yield
-
rescue => exception
-
rescue_with_handler(exception) || raise
-
end
-
-
1
private
-
1
def process(*)
-
handle_exceptions do
-
super
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_support/test_case"
-
1
require "rails-dom-testing"
-
-
1
module ActionMailer
-
1
class NonInferrableMailerError < ::StandardError
-
1
def initialize(name)
-
super "Unable to determine the mailer to test from #{name}. " \
-
"You'll need to specify it using tests YourMailer in your " \
-
"test case definition"
-
end
-
end
-
-
1
class TestCase < ActiveSupport::TestCase
-
1
module ClearTestDeliveries
-
1
extend ActiveSupport::Concern
-
-
1
included do
-
1
setup :clear_test_deliveries
-
1
teardown :clear_test_deliveries
-
end
-
-
1
private
-
-
1
def clear_test_deliveries
-
4
if ActionMailer::Base.delivery_method == :test
-
ActionMailer::Base.deliveries.clear
-
end
-
end
-
end
-
-
1
module Behavior
-
1
extend ActiveSupport::Concern
-
-
1
include ActiveSupport::Testing::ConstantLookup
-
1
include TestHelper
-
1
include Rails::Dom::Testing::Assertions::SelectorAssertions
-
1
include Rails::Dom::Testing::Assertions::DomAssertions
-
-
1
included do
-
1
class_attribute :_mailer_class
-
1
setup :initialize_test_deliveries
-
1
setup :set_expected_mail
-
1
teardown :restore_test_deliveries
-
1
ActiveSupport.run_load_hooks(:action_mailer_test_case, self)
-
end
-
-
1
module ClassMethods
-
1
def tests(mailer)
-
case mailer
-
when String, Symbol
-
self._mailer_class = mailer.to_s.camelize.constantize
-
when Module
-
self._mailer_class = mailer
-
else
-
raise NonInferrableMailerError.new(mailer)
-
end
-
end
-
-
1
def mailer_class
-
if mailer = _mailer_class
-
mailer
-
else
-
tests determine_default_mailer(name)
-
end
-
end
-
-
1
def determine_default_mailer(name)
-
mailer = determine_constant_from_test_name(name) do |constant|
-
Class === constant && constant < ActionMailer::Base
-
end
-
raise NonInferrableMailerError.new(name) if mailer.nil?
-
mailer
-
end
-
end
-
-
1
private
-
-
1
def initialize_test_deliveries
-
set_delivery_method :test
-
@old_perform_deliveries = ActionMailer::Base.perform_deliveries
-
ActionMailer::Base.perform_deliveries = true
-
ActionMailer::Base.deliveries.clear
-
end
-
-
1
def restore_test_deliveries
-
restore_delivery_method
-
ActionMailer::Base.perform_deliveries = @old_perform_deliveries
-
end
-
-
1
def set_delivery_method(method)
-
@old_delivery_method = ActionMailer::Base.delivery_method
-
ActionMailer::Base.delivery_method = method
-
end
-
-
1
def restore_delivery_method
-
ActionMailer::Base.deliveries.clear
-
ActionMailer::Base.delivery_method = @old_delivery_method
-
end
-
-
1
def set_expected_mail
-
@expected = Mail.new
-
@expected.content_type ["text", "plain", { "charset" => charset }]
-
@expected.mime_version = "1.0"
-
end
-
-
1
def charset
-
"UTF-8"
-
end
-
-
1
def encode(subject)
-
Mail::Encodings.q_value_encode(subject, charset)
-
end
-
-
1
def read_fixture(action)
-
IO.readlines(File.join(Rails.root, "test", "fixtures", self.class.mailer_class.name.underscore, action))
-
end
-
end
-
-
1
include Behavior
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_job"
-
-
1
module ActionMailer
-
# Provides helper methods for testing Action Mailer, including #assert_emails
-
# and #assert_no_emails.
-
1
module TestHelper
-
1
include ActiveJob::TestHelper
-
-
# Asserts that the number of emails sent matches the given number.
-
#
-
# def test_emails
-
# assert_emails 0
-
# ContactMailer.welcome.deliver_now
-
# assert_emails 1
-
# ContactMailer.welcome.deliver_now
-
# assert_emails 2
-
# end
-
#
-
# If a block is passed, that block should cause the specified number of
-
# emails to be sent.
-
#
-
# def test_emails_again
-
# assert_emails 1 do
-
# ContactMailer.welcome.deliver_now
-
# end
-
#
-
# assert_emails 2 do
-
# ContactMailer.welcome.deliver_now
-
# ContactMailer.welcome.deliver_now
-
# end
-
# end
-
1
def assert_emails(number)
-
if block_given?
-
original_count = ActionMailer::Base.deliveries.size
-
yield
-
new_count = ActionMailer::Base.deliveries.size
-
assert_equal number, new_count - original_count, "#{number} emails expected, but #{new_count - original_count} were sent"
-
else
-
assert_equal number, ActionMailer::Base.deliveries.size
-
end
-
end
-
-
# Asserts that no emails have been sent.
-
#
-
# def test_emails
-
# assert_no_emails
-
# ContactMailer.welcome.deliver_now
-
# assert_emails 1
-
# end
-
#
-
# If a block is passed, that block should not cause any emails to be sent.
-
#
-
# def test_emails_again
-
# assert_no_emails do
-
# # No emails should be sent from this block
-
# end
-
# end
-
#
-
# Note: This assertion is simply a shortcut for:
-
#
-
# assert_emails 0, &block
-
1
def assert_no_emails(&block)
-
assert_emails 0, &block
-
end
-
-
# Asserts that the number of emails enqueued for later delivery matches
-
# the given number.
-
#
-
# def test_emails
-
# assert_enqueued_emails 0
-
# ContactMailer.welcome.deliver_later
-
# assert_enqueued_emails 1
-
# ContactMailer.welcome.deliver_later
-
# assert_enqueued_emails 2
-
# end
-
#
-
# If a block is passed, that block should cause the specified number of
-
# emails to be enqueued.
-
#
-
# def test_emails_again
-
# assert_enqueued_emails 1 do
-
# ContactMailer.welcome.deliver_later
-
# end
-
#
-
# assert_enqueued_emails 2 do
-
# ContactMailer.welcome.deliver_later
-
# ContactMailer.welcome.deliver_later
-
# end
-
# end
-
1
def assert_enqueued_emails(number, &block)
-
assert_enqueued_jobs number, only: [ ActionMailer::DeliveryJob, ActionMailer::Parameterized::DeliveryJob ], &block
-
end
-
-
# Asserts that block should cause the specified email
-
# to be enqueued.
-
#
-
# def test_email_in_block
-
# assert_enqueued_email_with ContactMailer, :welcome do
-
# ContactMailer.welcome.deliver_later
-
# end
-
# end
-
#
-
# If +args+ is provided as a Hash, a parameterized email is matched.
-
#
-
# def test_parameterized_email
-
# assert_enqueued_email_with ContactMailer, :welcome,
-
# args: {email: 'user@example.com} do
-
# ContactMailer.with(email: 'user@example.com').welcome.deliver_later
-
# end
-
# end
-
1
def assert_enqueued_email_with(mailer, method, args: nil, queue: "mailers", &block)
-
if args.is_a? Hash
-
job = ActionMailer::Parameterized::DeliveryJob
-
args = [mailer.to_s, method.to_s, "deliver_now", args]
-
else
-
job = ActionMailer::DeliveryJob
-
args = [mailer.to_s, method.to_s, "deliver_now", *args]
-
end
-
-
assert_enqueued_with(job: job, args: args, queue: queue, &block)
-
end
-
-
# Asserts that no emails are enqueued for later delivery.
-
#
-
# def test_no_emails
-
# assert_no_enqueued_emails
-
# ContactMailer.welcome.deliver_later
-
# assert_enqueued_emails 1
-
# end
-
#
-
# If a block is provided, it should not cause any emails to be enqueued.
-
#
-
# def test_no_emails
-
# assert_no_enqueued_emails do
-
# # No emails should be enqueued from this block
-
# end
-
# end
-
1
def assert_no_enqueued_emails(&block)
-
assert_no_enqueued_jobs only: [ ActionMailer::DeliveryJob, ActionMailer::Parameterized::DeliveryJob ], &block
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "action_view"
-
1
require "action_controller"
-
1
require "action_controller/log_subscriber"
-
-
1
module ActionController
-
# API Controller is a lightweight version of <tt>ActionController::Base</tt>,
-
# created for applications that don't require all functionalities that a complete
-
# \Rails controller provides, allowing you to create controllers with just the
-
# features that you need for API only applications.
-
#
-
# An API Controller is different from a normal controller in the sense that
-
# by default it doesn't include a number of features that are usually required
-
# by browser access only: layouts and templates rendering, cookies, sessions,
-
# flash, assets, and so on. This makes the entire controller stack thinner,
-
# suitable for API applications. It doesn't mean you won't have such
-
# features if you need them: they're all available for you to include in
-
# your application, they're just not part of the default API controller stack.
-
#
-
# Normally, +ApplicationController+ is the only controller that inherits from
-
# <tt>ActionController::API</tt>. All other controllers in turn inherit from
-
# +ApplicationController+.
-
#
-
# A sample controller could look like this:
-
#
-
# class PostsController < ApplicationController
-
# def index
-
# posts = Post.all
-
# render json: posts
-
# end
-
# end
-
#
-
# Request, response, and parameters objects all work the exact same way as
-
# <tt>ActionController::Base</tt>.
-
#
-
# == Renders
-
#
-
# The default API Controller stack includes all renderers, which means you
-
# can use <tt>render :json</tt> and brothers freely in your controllers. Keep
-
# in mind that templates are not going to be rendered, so you need to ensure
-
# your controller is calling either <tt>render</tt> or <tt>redirect_to</tt> in
-
# all actions, otherwise it will return 204 No Content.
-
#
-
# def show
-
# post = Post.find(params[:id])
-
# render json: post
-
# end
-
#
-
# == Redirects
-
#
-
# Redirects are used to move from one action to another. You can use the
-
# <tt>redirect_to</tt> method in your controllers in the same way as in
-
# <tt>ActionController::Base</tt>. For example:
-
#
-
# def create
-
# redirect_to root_url and return if not_authorized?
-
# # do stuff here
-
# end
-
#
-
# == Adding New Behavior
-
#
-
# In some scenarios you may want to add back some functionality provided by
-
# <tt>ActionController::Base</tt> that is not present by default in
-
# <tt>ActionController::API</tt>, for instance <tt>MimeResponds</tt>. This
-
# module gives you the <tt>respond_to</tt> method. Adding it is quite simple,
-
# you just need to include the module in a specific controller or in
-
# +ApplicationController+ in case you want it available in your entire
-
# application:
-
#
-
# class ApplicationController < ActionController::API
-
# include ActionController::MimeResponds
-
# end
-
#
-
# class PostsController < ApplicationController
-
# def index
-
# posts = Post.all
-
#
-
# respond_to do |format|
-
# format.json { render json: posts }
-
# format.xml { render xml: posts }
-
# end
-
# end
-
# end
-
#
-
# Make sure to check the modules included in <tt>ActionController::Base</tt>
-
# if you want to use any other functionality that is not provided
-
# by <tt>ActionController::API</tt> out of the box.
-
1
class API < Metal
-
1
abstract!
-
-
# Shortcut helper that returns all the ActionController::API modules except
-
# the ones passed as arguments:
-
#
-
# class MyAPIBaseController < ActionController::Metal
-
# ActionController::API.without_modules(:ForceSSL, :UrlFor).each do |left|
-
# include left
-
# end
-
# end
-
#
-
# This gives better control over what you want to exclude and makes it easier
-
# to create an API controller class, instead of listing the modules required
-
# manually.
-
1
def self.without_modules(*modules)
-
modules = modules.map do |m|
-
m.is_a?(Symbol) ? ActionController.const_get(m) : m
-
end
-
-
MODULES - modules
-
end
-
-
1
MODULES = [
-
AbstractController::Rendering,
-
-
UrlFor,
-
Redirecting,
-
ApiRendering,
-
Renderers::All,
-
ConditionalGet,
-
BasicImplicitRender,
-
StrongParameters,
-
-
ForceSSL,
-
DataStreaming,
-
-
# Before callbacks should also be executed as early as possible, so
-
# also include them at the bottom.
-
AbstractController::Callbacks,
-
-
# Append rescue at the bottom to wrap as much as possible.
-
Rescue,
-
-
# Add instrumentations hooks at the bottom, to ensure they instrument
-
# all the methods properly.
-
Instrumentation,
-
-
# Params wrapper should come before instrumentation so they are
-
# properly showed in logs
-
ParamsWrapper
-
]
-
-
1
MODULES.each do |mod|
-
14
include mod
-
end
-
-
1
ActiveSupport.run_load_hooks(:action_controller_api, self)
-
1
ActiveSupport.run_load_hooks(:action_controller, self)
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActionController
-
1
module ApiRendering
-
1
extend ActiveSupport::Concern
-
-
1
included do
-
1
include Rendering
-
end
-
-
1
def render_to_body(options = {})
-
_process_options(options)
-
super
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActionController
-
1
module Testing
-
1
extend ActiveSupport::Concern
-
-
# Behavior specific to functional tests
-
1
module Functional # :nodoc:
-
1
def recycle!
-
@_url_options = nil
-
self.formats = nil
-
self.params = nil
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActionController
-
1
module TemplateAssertions
-
1
def assert_template(options = {}, message = nil)
-
raise NoMethodError,
-
"assert_template has been extracted to a gem. To continue using it,
-
add `gem 'rails-controller-testing'` to your Gemfile."
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "rack/session/abstract/id"
-
1
require "active_support/core_ext/hash/conversions"
-
1
require "active_support/core_ext/object/to_query"
-
1
require "active_support/core_ext/module/anonymous"
-
1
require "active_support/core_ext/module/redefine_method"
-
1
require "active_support/core_ext/hash/keys"
-
1
require "active_support/testing/constant_lookup"
-
1
require "action_controller/template_assertions"
-
1
require "rails-dom-testing"
-
-
1
module ActionController
-
1
class Metal
-
1
include Testing::Functional
-
end
-
-
1
module Live
-
# Disable controller / rendering threads in tests. User tests can access
-
# the database on the main thread, so they could open a txn, then the
-
# controller thread will open a new connection and try to access data
-
# that's only visible to the main thread's txn. This is the problem in #23483.
-
1
silence_redefinition_of_method :new_controller_thread
-
1
def new_controller_thread # :nodoc:
-
yield
-
end
-
end
-
-
# ActionController::TestCase will be deprecated and moved to a gem in Rails 5.1.
-
# Please use ActionDispatch::IntegrationTest going forward.
-
1
class TestRequest < ActionDispatch::TestRequest #:nodoc:
-
1
DEFAULT_ENV = ActionDispatch::TestRequest::DEFAULT_ENV.dup
-
1
DEFAULT_ENV.delete "PATH_INFO"
-
-
1
def self.new_session
-
TestSession.new
-
end
-
-
1
attr_reader :controller_class
-
-
# Create a new test request with default `env` values.
-
1
def self.create(controller_class)
-
env = {}
-
env = Rails.application.env_config.merge(env) if defined?(Rails.application) && Rails.application
-
env["rack.request.cookie_hash"] = {}.with_indifferent_access
-
new(default_env.merge(env), new_session, controller_class)
-
end
-
-
1
def self.default_env
-
DEFAULT_ENV
-
end
-
1
private_class_method :default_env
-
-
1
def initialize(env, session, controller_class)
-
super(env)
-
-
self.session = session
-
self.session_options = TestSession::DEFAULT_OPTIONS.dup
-
@controller_class = controller_class
-
@custom_param_parsers = {
-
xml: lambda { |raw_post| Hash.from_xml(raw_post)["hash"] }
-
}
-
end
-
-
1
def query_string=(string)
-
set_header Rack::QUERY_STRING, string
-
end
-
-
1
def content_type=(type)
-
set_header "CONTENT_TYPE", type
-
end
-
-
1
def assign_parameters(routes, controller_path, action, parameters, generated_path, query_string_keys)
-
non_path_parameters = {}
-
path_parameters = {}
-
-
parameters.each do |key, value|
-
if query_string_keys.include?(key)
-
non_path_parameters[key] = value
-
else
-
if value.is_a?(Array)
-
value = value.map(&:to_param)
-
else
-
value = value.to_param
-
end
-
-
path_parameters[key] = value
-
end
-
end
-
-
if get?
-
if query_string.blank?
-
self.query_string = non_path_parameters.to_query
-
end
-
else
-
if ENCODER.should_multipart?(non_path_parameters)
-
self.content_type = ENCODER.content_type
-
data = ENCODER.build_multipart non_path_parameters
-
else
-
fetch_header("CONTENT_TYPE") do |k|
-
set_header k, "application/x-www-form-urlencoded"
-
end
-
-
case content_mime_type.to_sym
-
when nil
-
raise "Unknown Content-Type: #{content_type}"
-
when :json
-
data = ActiveSupport::JSON.encode(non_path_parameters)
-
when :xml
-
data = non_path_parameters.to_xml
-
when :url_encoded_form
-
data = non_path_parameters.to_query
-
else
-
@custom_param_parsers[content_mime_type.symbol] = ->(_) { non_path_parameters }
-
data = non_path_parameters.to_query
-
end
-
end
-
-
data_stream = StringIO.new(data)
-
set_header "CONTENT_LENGTH", data_stream.length.to_s
-
set_header "rack.input", data_stream
-
end
-
-
fetch_header("PATH_INFO") do |k|
-
set_header k, generated_path
-
end
-
path_parameters[:controller] = controller_path
-
path_parameters[:action] = action
-
-
self.path_parameters = path_parameters
-
end
-
-
1
ENCODER = Class.new do
-
1
include Rack::Test::Utils
-
-
1
def should_multipart?(params)
-
# FIXME: lifted from Rack-Test. We should push this separation upstream.
-
multipart = false
-
query = lambda { |value|
-
case value
-
when Array
-
value.each(&query)
-
when Hash
-
value.values.each(&query)
-
when Rack::Test::UploadedFile
-
multipart = true
-
end
-
}
-
params.values.each(&query)
-
multipart
-
end
-
-
1
public :build_multipart
-
-
1
def content_type
-
"multipart/form-data; boundary=#{Rack::Test::MULTIPART_BOUNDARY}"
-
end
-
end.new
-
-
1
private
-
-
1
def params_parsers
-
super.merge @custom_param_parsers
-
end
-
end
-
-
1
class LiveTestResponse < Live::Response
-
# Was the response successful?
-
1
alias_method :success?, :successful?
-
-
# Was the URL not found?
-
1
alias_method :missing?, :not_found?
-
-
# Was there a server-side error?
-
1
alias_method :error?, :server_error?
-
end
-
-
# Methods #destroy and #load! are overridden to avoid calling methods on the
-
# @store object, which does not exist for the TestSession class.
-
1
class TestSession < Rack::Session::Abstract::SessionHash #:nodoc:
-
1
DEFAULT_OPTIONS = Rack::Session::Abstract::Persisted::DEFAULT_OPTIONS
-
-
1
def initialize(session = {})
-
super(nil, nil)
-
@id = SecureRandom.hex(16)
-
@data = stringify_keys(session)
-
@loaded = true
-
end
-
-
1
def exists?
-
true
-
end
-
-
1
def keys
-
@data.keys
-
end
-
-
1
def values
-
@data.values
-
end
-
-
1
def destroy
-
clear
-
end
-
-
1
def fetch(key, *args, &block)
-
@data.fetch(key.to_s, *args, &block)
-
end
-
-
1
private
-
-
1
def load!
-
@id
-
end
-
end
-
-
# Superclass for ActionController functional tests. Functional tests allow you to
-
# test a single controller action per test method.
-
#
-
# == Use integration style controller tests over functional style controller tests.
-
#
-
# Rails discourages the use of functional tests in favor of integration tests
-
# (use ActionDispatch::IntegrationTest).
-
#
-
# New Rails applications no longer generate functional style controller tests and they should
-
# only be used for backward compatibility. Integration style controller tests perform actual
-
# requests, whereas functional style controller tests merely simulate a request. Besides,
-
# integration tests are as fast as functional tests and provide lot of helpers such as +as+,
-
# +parsed_body+ for effective testing of controller actions including even API endpoints.
-
#
-
# == Basic example
-
#
-
# Functional tests are written as follows:
-
# 1. First, one uses the +get+, +post+, +patch+, +put+, +delete+ or +head+ method to simulate
-
# an HTTP request.
-
# 2. Then, one asserts whether the current state is as expected. "State" can be anything:
-
# the controller's HTTP response, the database contents, etc.
-
#
-
# For example:
-
#
-
# class BooksControllerTest < ActionController::TestCase
-
# def test_create
-
# # Simulate a POST response with the given HTTP parameters.
-
# post(:create, params: { book: { title: "Love Hina" }})
-
#
-
# # Asserts that the controller tried to redirect us to
-
# # the created book's URI.
-
# assert_response :found
-
#
-
# # Asserts that the controller really put the book in the database.
-
# assert_not_nil Book.find_by(title: "Love Hina")
-
# end
-
# end
-
#
-
# You can also send a real document in the simulated HTTP request.
-
#
-
# def test_create
-
# json = {book: { title: "Love Hina" }}.to_json
-
# post :create, body: json
-
# end
-
#
-
# == Special instance variables
-
#
-
# ActionController::TestCase will also automatically provide the following instance
-
# variables for use in the tests:
-
#
-
# <b>@controller</b>::
-
# The controller instance that will be tested.
-
# <b>@request</b>::
-
# An ActionController::TestRequest, representing the current HTTP
-
# request. You can modify this object before sending the HTTP request. For example,
-
# you might want to set some session properties before sending a GET request.
-
# <b>@response</b>::
-
# An ActionDispatch::TestResponse object, representing the response
-
# of the last HTTP response. In the above example, <tt>@response</tt> becomes valid
-
# after calling +post+. If the various assert methods are not sufficient, then you
-
# may use this object to inspect the HTTP response in detail.
-
#
-
# (Earlier versions of \Rails required each functional test to subclass
-
# Test::Unit::TestCase and define @controller, @request, @response in +setup+.)
-
#
-
# == Controller is automatically inferred
-
#
-
# ActionController::TestCase will automatically infer the controller under test
-
# from the test class name. If the controller cannot be inferred from the test
-
# class name, you can explicitly set it with +tests+.
-
#
-
# class SpecialEdgeCaseWidgetsControllerTest < ActionController::TestCase
-
# tests WidgetController
-
# end
-
#
-
# == \Testing controller internals
-
#
-
# In addition to these specific assertions, you also have easy access to various collections that the regular test/unit assertions
-
# can be used against. These collections are:
-
#
-
# * session: Objects being saved in the session.
-
# * flash: The flash objects currently in the session.
-
# * cookies: \Cookies being sent to the user on this request.
-
#
-
# These collections can be used just like any other hash:
-
#
-
# assert_equal "Dave", cookies[:name] # makes sure that a cookie called :name was set as "Dave"
-
# assert flash.empty? # makes sure that there's nothing in the flash
-
#
-
# On top of the collections, you have the complete URL that a given action redirected to available in <tt>redirect_to_url</tt>.
-
#
-
# For redirects within the same controller, you can even call follow_redirect and the redirect will be followed, triggering another
-
# action call which can then be asserted against.
-
#
-
# == Manipulating session and cookie variables
-
#
-
# Sometimes you need to set up the session and cookie variables for a test.
-
# To do this just assign a value to the session or cookie collection:
-
#
-
# session[:key] = "value"
-
# cookies[:key] = "value"
-
#
-
# To clear the cookies for a test just clear the cookie collection:
-
#
-
# cookies.clear
-
#
-
# == \Testing named routes
-
#
-
# If you're using named routes, they can be easily tested using the original named routes' methods straight in the test case.
-
#
-
# assert_redirected_to page_url(title: 'foo')
-
1
class TestCase < ActiveSupport::TestCase
-
1
module Behavior
-
1
extend ActiveSupport::Concern
-
1
include ActionDispatch::TestProcess
-
1
include ActiveSupport::Testing::ConstantLookup
-
1
include Rails::Dom::Testing::Assertions
-
-
1
attr_reader :response, :request
-
-
1
module ClassMethods
-
# Sets the controller class name. Useful if the name can't be inferred from test class.
-
# Normalizes +controller_class+ before using.
-
#
-
# tests WidgetController
-
# tests :widget
-
# tests 'widget'
-
1
def tests(controller_class)
-
case controller_class
-
when String, Symbol
-
self.controller_class = "#{controller_class.to_s.camelize}Controller".constantize
-
when Class
-
self.controller_class = controller_class
-
else
-
raise ArgumentError, "controller class must be a String, Symbol, or Class"
-
end
-
end
-
-
1
def controller_class=(new_class)
-
self._controller_class = new_class
-
end
-
-
1
def controller_class
-
if current_controller_class = _controller_class
-
current_controller_class
-
else
-
self.controller_class = determine_default_controller_class(name)
-
end
-
end
-
-
1
def determine_default_controller_class(name)
-
determine_constant_from_test_name(name) do |constant|
-
Class === constant && constant < ActionController::Metal
-
end
-
end
-
end
-
-
# Simulate a GET request with the given parameters.
-
#
-
# - +action+: The controller action to call.
-
# - +params+: The hash with HTTP parameters that you want to pass. This may be +nil+.
-
# - +body+: The request body with a string that is appropriately encoded
-
# (<tt>application/x-www-form-urlencoded</tt> or <tt>multipart/form-data</tt>).
-
# - +session+: A hash of parameters to store in the session. This may be +nil+.
-
# - +flash+: A hash of parameters to store in the flash. This may be +nil+.
-
#
-
# You can also simulate POST, PATCH, PUT, DELETE, and HEAD requests with
-
# +post+, +patch+, +put+, +delete+, and +head+.
-
# Example sending parameters, session and setting a flash message:
-
#
-
# get :show,
-
# params: { id: 7 },
-
# session: { user_id: 1 },
-
# flash: { notice: 'This is flash message' }
-
#
-
# Note that the request method is not verified. The different methods are
-
# available to make the tests more expressive.
-
1
def get(action, **args)
-
res = process(action, method: "GET", **args)
-
cookies.update res.cookies
-
res
-
end
-
-
# Simulate a POST request with the given parameters and set/volley the response.
-
# See +get+ for more details.
-
1
def post(action, **args)
-
process(action, method: "POST", **args)
-
end
-
-
# Simulate a PATCH request with the given parameters and set/volley the response.
-
# See +get+ for more details.
-
1
def patch(action, **args)
-
process(action, method: "PATCH", **args)
-
end
-
-
# Simulate a PUT request with the given parameters and set/volley the response.
-
# See +get+ for more details.
-
1
def put(action, **args)
-
process(action, method: "PUT", **args)
-
end
-
-
# Simulate a DELETE request with the given parameters and set/volley the response.
-
# See +get+ for more details.
-
1
def delete(action, **args)
-
process(action, method: "DELETE", **args)
-
end
-
-
# Simulate a HEAD request with the given parameters and set/volley the response.
-
# See +get+ for more details.
-
1
def head(action, **args)
-
process(action, method: "HEAD", **args)
-
end
-
-
# Simulate an HTTP request to +action+ by specifying request method,
-
# parameters and set/volley the response.
-
#
-
# - +action+: The controller action to call.
-
# - +method+: Request method used to send the HTTP request. Possible values
-
# are +GET+, +POST+, +PATCH+, +PUT+, +DELETE+, +HEAD+. Defaults to +GET+. Can be a symbol.
-
# - +params+: The hash with HTTP parameters that you want to pass. This may be +nil+.
-
# - +body+: The request body with a string that is appropriately encoded
-
# (<tt>application/x-www-form-urlencoded</tt> or <tt>multipart/form-data</tt>).
-
# - +session+: A hash of parameters to store in the session. This may be +nil+.
-
# - +flash+: A hash of parameters to store in the flash. This may be +nil+.
-
# - +format+: Request format. Defaults to +nil+. Can be string or symbol.
-
# - +as+: Content type. Defaults to +nil+. Must be a symbol that corresponds
-
# to a mime type.
-
#
-
# Example calling +create+ action and sending two params:
-
#
-
# process :create,
-
# method: 'POST',
-
# params: {
-
# user: { name: 'Gaurish Sharma', email: 'user@example.com' }
-
# },
-
# session: { user_id: 1 },
-
# flash: { notice: 'This is flash message' }
-
#
-
# To simulate +GET+, +POST+, +PATCH+, +PUT+, +DELETE+ and +HEAD+ requests
-
# prefer using #get, #post, #patch, #put, #delete and #head methods
-
# respectively which will make tests more expressive.
-
#
-
# Note that the request method is not verified.
-
1
def process(action, method: "GET", params: nil, session: nil, body: nil, flash: {}, format: nil, xhr: false, as: nil)
-
check_required_ivars
-
-
http_method = method.to_s.upcase
-
-
@html_document = nil
-
-
cookies.update(@request.cookies)
-
cookies.update_cookies_from_jar
-
@request.set_header "HTTP_COOKIE", cookies.to_header
-
@request.delete_header "action_dispatch.cookies"
-
-
@request = TestRequest.new scrub_env!(@request.env), @request.session, @controller.class
-
@response = build_response @response_klass
-
@response.request = @request
-
@controller.recycle!
-
-
if body
-
@request.set_header "RAW_POST_DATA", body
-
end
-
-
@request.set_header "REQUEST_METHOD", http_method
-
-
if as
-
@request.content_type = Mime[as].to_s
-
format ||= as
-
end
-
-
parameters = (params || {}).symbolize_keys
-
-
if format
-
parameters[:format] = format
-
end
-
-
generated_extras = @routes.generate_extras(parameters.merge(controller: controller_class_name, action: action.to_s))
-
generated_path = generated_path(generated_extras)
-
query_string_keys = query_parameter_names(generated_extras)
-
-
@request.assign_parameters(@routes, controller_class_name, action.to_s, parameters, generated_path, query_string_keys)
-
-
@request.session.update(session) if session
-
@request.flash.update(flash || {})
-
-
if xhr
-
@request.set_header "HTTP_X_REQUESTED_WITH", "XMLHttpRequest"
-
@request.fetch_header("HTTP_ACCEPT") do |k|
-
@request.set_header k, [Mime[:js], Mime[:html], Mime[:xml], "text/xml", "*/*"].join(", ")
-
end
-
end
-
-
@request.fetch_header("SCRIPT_NAME") do |k|
-
@request.set_header k, @controller.config.relative_url_root
-
end
-
-
begin
-
@controller.recycle!
-
@controller.dispatch(action, @request, @response)
-
ensure
-
@request = @controller.request
-
@response = @controller.response
-
-
if @request.have_cookie_jar?
-
unless @request.cookie_jar.committed?
-
@request.cookie_jar.write(@response)
-
cookies.update(@request.cookie_jar.instance_variable_get(:@cookies))
-
end
-
end
-
@response.prepare!
-
-
if flash_value = @request.flash.to_session_value
-
@request.session["flash"] = flash_value
-
else
-
@request.session.delete("flash")
-
end
-
-
if xhr
-
@request.delete_header "HTTP_X_REQUESTED_WITH"
-
@request.delete_header "HTTP_ACCEPT"
-
end
-
@request.query_string = ""
-
-
@response.sent!
-
end
-
-
@response
-
end
-
-
1
def controller_class_name
-
@controller.class.anonymous? ? "anonymous" : @controller.class.controller_path
-
end
-
-
1
def generated_path(generated_extras)
-
generated_extras[0]
-
end
-
-
1
def query_parameter_names(generated_extras)
-
generated_extras[1] + [:controller, :action]
-
end
-
-
1
def setup_controller_request_and_response
-
@controller = nil unless defined? @controller
-
-
@response_klass = ActionDispatch::TestResponse
-
-
if klass = self.class.controller_class
-
if klass < ActionController::Live
-
@response_klass = LiveTestResponse
-
end
-
unless @controller
-
begin
-
@controller = klass.new
-
rescue
-
warn "could not construct controller #{klass}" if $VERBOSE
-
end
-
end
-
end
-
-
@request = TestRequest.create(@controller.class)
-
@response = build_response @response_klass
-
@response.request = @request
-
-
if @controller
-
@controller.request = @request
-
@controller.params = {}
-
end
-
end
-
-
1
def build_response(klass)
-
klass.create
-
end
-
-
1
included do
-
1
include ActionController::TemplateAssertions
-
1
include ActionDispatch::Assertions
-
1
class_attribute :_controller_class
-
1
setup :setup_controller_request_and_response
-
1
ActiveSupport.run_load_hooks(:action_controller_test_case, self)
-
end
-
-
1
private
-
-
1
def scrub_env!(env)
-
env.delete_if { |k, v| k =~ /^(action_dispatch|rack)\.request/ }
-
env.delete_if { |k, v| k =~ /^action_dispatch\.rescue/ }
-
env.delete "action_dispatch.request.query_parameters"
-
env.delete "action_dispatch.request.request_parameters"
-
env["rack.input"] = StringIO.new
-
env.delete "CONTENT_LENGTH"
-
env.delete "RAW_POST_DATA"
-
env
-
end
-
-
1
def document_root_element
-
html_document.root
-
end
-
-
1
def check_required_ivars
-
# Sanity check for required instance variables so we can give an
-
# understandable error message.
-
[:@routes, :@controller, :@request, :@response].each do |iv_name|
-
if !instance_variable_defined?(iv_name) || instance_variable_get(iv_name).nil?
-
raise "#{iv_name} is nil: make sure you set it in your test's setup method."
-
end
-
end
-
end
-
end
-
-
1
include Behavior
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActionDispatch
-
# Provides callbacks to be executed before and after dispatching the request.
-
1
class Callbacks
-
1
include ActiveSupport::Callbacks
-
-
1
define_callbacks :call
-
-
1
class << self
-
1
def before(*args, &block)
-
set_callback(:call, :before, *args, &block)
-
end
-
-
1
def after(*args, &block)
-
set_callback(:call, :after, *args, &block)
-
end
-
end
-
-
1
def initialize(app)
-
1
@app = app
-
end
-
-
1
def call(env)
-
2
error = nil
-
2
result = run_callbacks :call do
-
2
begin
-
2
@app.call(env)
-
rescue => error
-
end
-
end
-
2
raise error if error
-
2
result
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "action_dispatch/http/request"
-
1
require "action_dispatch/middleware/exception_wrapper"
-
1
require "action_dispatch/routing/inspector"
-
1
require "action_view"
-
1
require "action_view/base"
-
-
1
require "pp"
-
-
1
module ActionDispatch
-
# This middleware is responsible for logging exceptions and
-
# showing a debugging page in case the request is local.
-
1
class DebugExceptions
-
1
RESCUES_TEMPLATE_PATH = File.expand_path("templates", __dir__)
-
-
1
class DebugView < ActionView::Base
-
1
def debug_params(params)
-
clean_params = params.clone
-
clean_params.delete("action")
-
clean_params.delete("controller")
-
-
if clean_params.empty?
-
"None"
-
else
-
PP.pp(clean_params, "".dup, 200)
-
end
-
end
-
-
1
def debug_headers(headers)
-
if headers.present?
-
headers.inspect.gsub(",", ",\n")
-
else
-
"None"
-
end
-
end
-
-
1
def debug_hash(object)
-
object.to_hash.sort_by { |k, _| k.to_s }.map { |k, v| "#{k}: #{v.inspect rescue $!.message}" }.join("\n")
-
end
-
-
1
def render(*)
-
logger = ActionView::Base.logger
-
-
if logger && logger.respond_to?(:silence)
-
logger.silence { super }
-
else
-
super
-
end
-
end
-
end
-
-
1
def initialize(app, routes_app = nil, response_format = :default)
-
1
@app = app
-
1
@routes_app = routes_app
-
1
@response_format = response_format
-
end
-
-
1
def call(env)
-
2
request = ActionDispatch::Request.new env
-
2
_, headers, body = response = @app.call(env)
-
-
2
if headers["X-Cascade"] == "pass"
-
body.close if body.respond_to?(:close)
-
raise ActionController::RoutingError, "No route matches [#{env['REQUEST_METHOD']}] #{env['PATH_INFO'].inspect}"
-
end
-
-
2
response
-
rescue Exception => exception
-
raise exception unless request.show_exceptions?
-
render_exception(request, exception)
-
end
-
-
1
private
-
-
1
def render_exception(request, exception)
-
backtrace_cleaner = request.get_header("action_dispatch.backtrace_cleaner")
-
wrapper = ExceptionWrapper.new(backtrace_cleaner, exception)
-
log_error(request, wrapper)
-
-
if request.get_header("action_dispatch.show_detailed_exceptions")
-
content_type = request.formats.first
-
-
if api_request?(content_type)
-
render_for_api_request(content_type, wrapper)
-
else
-
render_for_browser_request(request, wrapper)
-
end
-
else
-
raise exception
-
end
-
end
-
-
1
def render_for_browser_request(request, wrapper)
-
template = create_template(request, wrapper)
-
file = "rescues/#{wrapper.rescue_template}"
-
-
if request.xhr?
-
body = template.render(template: file, layout: false, formats: [:text])
-
format = "text/plain"
-
else
-
body = template.render(template: file, layout: "rescues/layout")
-
format = "text/html"
-
end
-
render(wrapper.status_code, body, format)
-
end
-
-
1
def render_for_api_request(content_type, wrapper)
-
body = {
-
status: wrapper.status_code,
-
error: Rack::Utils::HTTP_STATUS_CODES.fetch(
-
wrapper.status_code,
-
Rack::Utils::HTTP_STATUS_CODES[500]
-
),
-
exception: wrapper.exception.inspect,
-
traces: wrapper.traces
-
}
-
-
to_format = "to_#{content_type.to_sym}"
-
-
if content_type && body.respond_to?(to_format)
-
formatted_body = body.public_send(to_format)
-
format = content_type
-
else
-
formatted_body = body.to_json
-
format = Mime[:json]
-
end
-
-
render(wrapper.status_code, formatted_body, format)
-
end
-
-
1
def create_template(request, wrapper)
-
traces = wrapper.traces
-
-
trace_to_show = "Application Trace"
-
if traces[trace_to_show].empty? && wrapper.rescue_template != "routing_error"
-
trace_to_show = "Full Trace"
-
end
-
-
if source_to_show = traces[trace_to_show].first
-
source_to_show_id = source_to_show[:id]
-
end
-
-
DebugView.new([RESCUES_TEMPLATE_PATH],
-
request: request,
-
exception: wrapper.exception,
-
traces: traces,
-
show_source_idx: source_to_show_id,
-
trace_to_show: trace_to_show,
-
routes_inspector: routes_inspector(wrapper.exception),
-
source_extracts: wrapper.source_extracts,
-
line_number: wrapper.line_number,
-
file: wrapper.file
-
)
-
end
-
-
1
def render(status, body, format)
-
[status, { "Content-Type" => "#{format}; charset=#{Response.default_charset}", "Content-Length" => body.bytesize.to_s }, [body]]
-
end
-
-
1
def log_error(request, wrapper)
-
logger = logger(request)
-
return unless logger
-
-
exception = wrapper.exception
-
-
trace = wrapper.application_trace
-
trace = wrapper.framework_trace if trace.empty?
-
-
ActiveSupport::Deprecation.silence do
-
logger.fatal " "
-
logger.fatal "#{exception.class} (#{exception.message}):"
-
log_array logger, exception.annoted_source_code if exception.respond_to?(:annoted_source_code)
-
logger.fatal " "
-
log_array logger, trace
-
end
-
end
-
-
1
def log_array(logger, array)
-
if logger.formatter && logger.formatter.respond_to?(:tags_text)
-
logger.fatal array.join("\n#{logger.formatter.tags_text}")
-
else
-
logger.fatal array.join("\n")
-
end
-
end
-
-
1
def logger(request)
-
request.logger || ActionView::Base.logger || stderr_logger
-
end
-
-
1
def stderr_logger
-
@stderr_logger ||= ActiveSupport::Logger.new($stderr)
-
end
-
-
1
def routes_inspector(exception)
-
if @routes_app.respond_to?(:routes) && (exception.is_a?(ActionController::RoutingError) || exception.is_a?(ActionView::Template::Error))
-
ActionDispatch::Routing::RoutesInspector.new(@routes_app.routes.routes)
-
end
-
end
-
-
1
def api_request?(content_type)
-
@response_format == :api && !content_type.html?
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_support/core_ext/module/attribute_accessors"
-
1
require "rack/utils"
-
-
1
module ActionDispatch
-
1
class ExceptionWrapper
-
1
cattr_accessor :rescue_responses, default: Hash.new(:internal_server_error).merge!(
-
"ActionController::RoutingError" => :not_found,
-
"AbstractController::ActionNotFound" => :not_found,
-
"ActionController::MethodNotAllowed" => :method_not_allowed,
-
"ActionController::UnknownHttpMethod" => :method_not_allowed,
-
"ActionController::NotImplemented" => :not_implemented,
-
"ActionController::UnknownFormat" => :not_acceptable,
-
"ActionController::InvalidAuthenticityToken" => :unprocessable_entity,
-
"ActionController::InvalidCrossOriginRequest" => :unprocessable_entity,
-
"ActionDispatch::Http::Parameters::ParseError" => :bad_request,
-
"ActionController::BadRequest" => :bad_request,
-
"ActionController::ParameterMissing" => :bad_request,
-
"Rack::QueryParser::ParameterTypeError" => :bad_request,
-
"Rack::QueryParser::InvalidParameterError" => :bad_request
-
)
-
-
1
cattr_accessor :rescue_templates, default: Hash.new("diagnostics").merge!(
-
"ActionView::MissingTemplate" => "missing_template",
-
"ActionController::RoutingError" => "routing_error",
-
"AbstractController::ActionNotFound" => "unknown_action",
-
"ActiveRecord::StatementInvalid" => "invalid_statement",
-
"ActionView::Template::Error" => "template_error"
-
)
-
-
1
attr_reader :backtrace_cleaner, :exception, :line_number, :file
-
-
1
def initialize(backtrace_cleaner, exception)
-
@backtrace_cleaner = backtrace_cleaner
-
@exception = original_exception(exception)
-
-
expand_backtrace if exception.is_a?(SyntaxError) || exception.cause.is_a?(SyntaxError)
-
end
-
-
1
def rescue_template
-
@@rescue_templates[@exception.class.name]
-
end
-
-
1
def status_code
-
self.class.status_code_for_exception(@exception.class.name)
-
end
-
-
1
def application_trace
-
clean_backtrace(:silent)
-
end
-
-
1
def framework_trace
-
clean_backtrace(:noise)
-
end
-
-
1
def full_trace
-
clean_backtrace(:all)
-
end
-
-
1
def traces
-
application_trace_with_ids = []
-
framework_trace_with_ids = []
-
full_trace_with_ids = []
-
-
full_trace.each_with_index do |trace, idx|
-
trace_with_id = { id: idx, trace: trace }
-
-
if application_trace.include?(trace)
-
application_trace_with_ids << trace_with_id
-
else
-
framework_trace_with_ids << trace_with_id
-
end
-
-
full_trace_with_ids << trace_with_id
-
end
-
-
{
-
"Application Trace" => application_trace_with_ids,
-
"Framework Trace" => framework_trace_with_ids,
-
"Full Trace" => full_trace_with_ids
-
}
-
end
-
-
1
def self.status_code_for_exception(class_name)
-
Rack::Utils.status_code(@@rescue_responses[class_name])
-
end
-
-
1
def source_extracts
-
backtrace.map do |trace|
-
file, line_number = extract_file_and_line_number(trace)
-
-
{
-
code: source_fragment(file, line_number),
-
line_number: line_number
-
}
-
end
-
end
-
-
1
private
-
-
1
def backtrace
-
Array(@exception.backtrace)
-
end
-
-
1
def original_exception(exception)
-
if @@rescue_responses.has_key?(exception.cause.class.name)
-
exception.cause
-
else
-
exception
-
end
-
end
-
-
1
def clean_backtrace(*args)
-
if backtrace_cleaner
-
backtrace_cleaner.clean(backtrace, *args)
-
else
-
backtrace
-
end
-
end
-
-
1
def source_fragment(path, line)
-
return unless Rails.respond_to?(:root) && Rails.root
-
full_path = Rails.root.join(path)
-
if File.exist?(full_path)
-
File.open(full_path, "r") do |file|
-
start = [line - 3, 0].max
-
lines = file.each_line.drop(start).take(6)
-
Hash[*(start + 1..(lines.count + start)).zip(lines).flatten]
-
end
-
end
-
end
-
-
1
def extract_file_and_line_number(trace)
-
# Split by the first colon followed by some digits, which works for both
-
# Windows and Unix path styles.
-
file, line = trace.match(/^(.+?):(\d+).*$/, &:captures) || trace
-
[file, line.to_i]
-
end
-
-
1
def expand_backtrace
-
@exception.backtrace.unshift(
-
@exception.to_s.split("\n")
-
).flatten!
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "rack/body_proxy"
-
-
1
module ActionDispatch
-
1
class Executor
-
1
def initialize(app, executor)
-
2
@app, @executor = app, executor
-
end
-
-
1
def call(env)
-
4
state = @executor.run!
-
4
begin
-
4
response = @app.call(env)
-
8
returned = response << ::Rack::BodyProxy.new(response.pop) { state.complete! }
-
ensure
-
4
state.complete! unless returned
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_support/core_ext/hash/keys"
-
-
1
module ActionDispatch
-
# The flash provides a way to pass temporary primitive-types (String, Array, Hash) between actions. Anything you place in the flash will be exposed
-
# to the very next action and then cleared out. This is a great way of doing notices and alerts, such as a create
-
# action that sets <tt>flash[:notice] = "Post successfully created"</tt> before redirecting to a display action that can
-
# then expose the flash to its template. Actually, that exposure is automatically done.
-
#
-
# class PostsController < ActionController::Base
-
# def create
-
# # save post
-
# flash[:notice] = "Post successfully created"
-
# redirect_to @post
-
# end
-
#
-
# def show
-
# # doesn't need to assign the flash notice to the template, that's done automatically
-
# end
-
# end
-
#
-
# show.html.erb
-
# <% if flash[:notice] %>
-
# <div class="notice"><%= flash[:notice] %></div>
-
# <% end %>
-
#
-
# Since the +notice+ and +alert+ keys are a common idiom, convenience accessors are available:
-
#
-
# flash.alert = "You must be logged in"
-
# flash.notice = "Post successfully created"
-
#
-
# This example places a string in the flash. And of course, you can put as many as you like at a time too. If you want to pass
-
# non-primitive types, you will have to handle that in your application. Example: To show messages with links, you will have to
-
# use sanitize helper.
-
#
-
# Just remember: They'll be gone by the time the next action has been performed.
-
#
-
# See docs on the FlashHash class for more details about the flash.
-
1
class Flash
-
1
KEY = "action_dispatch.request.flash_hash".freeze
-
-
1
module RequestMethods
-
# Access the contents of the flash. Use <tt>flash["notice"]</tt> to
-
# read a notice you put there or <tt>flash["notice"] = "hello"</tt>
-
# to put a new one.
-
1
def flash
-
flash = flash_hash
-
return flash if flash
-
self.flash = Flash::FlashHash.from_session_value(session["flash"])
-
end
-
-
1
def flash=(flash)
-
set_header Flash::KEY, flash
-
end
-
-
1
def flash_hash # :nodoc:
-
2
get_header Flash::KEY
-
end
-
-
1
def commit_flash # :nodoc:
-
2
session = self.session || {}
-
2
flash_hash = self.flash_hash
-
-
2
if flash_hash && (flash_hash.present? || session.key?("flash"))
-
session["flash"] = flash_hash.to_session_value
-
self.flash = flash_hash.dup
-
end
-
-
2
if (!session.respond_to?(:loaded?) || session.loaded?) && # reset_session uses {}, which doesn't implement #loaded?
-
session.key?("flash") && session["flash"].nil?
-
session.delete("flash")
-
end
-
end
-
-
1
def reset_session # :nodoc:
-
super
-
self.flash = nil
-
end
-
end
-
-
1
class FlashNow #:nodoc:
-
1
attr_accessor :flash
-
-
1
def initialize(flash)
-
@flash = flash
-
end
-
-
1
def []=(k, v)
-
k = k.to_s
-
@flash[k] = v
-
@flash.discard(k)
-
v
-
end
-
-
1
def [](k)
-
@flash[k.to_s]
-
end
-
-
# Convenience accessor for <tt>flash.now[:alert]=</tt>.
-
1
def alert=(message)
-
self[:alert] = message
-
end
-
-
# Convenience accessor for <tt>flash.now[:notice]=</tt>.
-
1
def notice=(message)
-
self[:notice] = message
-
end
-
end
-
-
1
class FlashHash
-
1
include Enumerable
-
-
1
def self.from_session_value(value) #:nodoc:
-
case value
-
when FlashHash # Rails 3.1, 3.2
-
flashes = value.instance_variable_get(:@flashes)
-
if discard = value.instance_variable_get(:@used)
-
flashes.except!(*discard)
-
end
-
new(flashes, flashes.keys)
-
when Hash # Rails 4.0
-
flashes = value["flashes"]
-
if discard = value["discard"]
-
flashes.except!(*discard)
-
end
-
new(flashes, flashes.keys)
-
else
-
new
-
end
-
end
-
-
# Builds a hash containing the flashes to keep for the next request.
-
# If there are none to keep, returns +nil+.
-
1
def to_session_value #:nodoc:
-
flashes_to_keep = @flashes.except(*@discard)
-
return nil if flashes_to_keep.empty?
-
{ "discard" => [], "flashes" => flashes_to_keep }
-
end
-
-
1
def initialize(flashes = {}, discard = []) #:nodoc:
-
@discard = Set.new(stringify_array(discard))
-
@flashes = flashes.stringify_keys
-
@now = nil
-
end
-
-
1
def initialize_copy(other)
-
if other.now_is_loaded?
-
@now = other.now.dup
-
@now.flash = self
-
end
-
super
-
end
-
-
1
def []=(k, v)
-
k = k.to_s
-
@discard.delete k
-
@flashes[k] = v
-
end
-
-
1
def [](k)
-
@flashes[k.to_s]
-
end
-
-
1
def update(h) #:nodoc:
-
@discard.subtract stringify_array(h.keys)
-
@flashes.update h.stringify_keys
-
self
-
end
-
-
1
def keys
-
@flashes.keys
-
end
-
-
1
def key?(name)
-
@flashes.key? name.to_s
-
end
-
-
1
def delete(key)
-
key = key.to_s
-
@discard.delete key
-
@flashes.delete key
-
self
-
end
-
-
1
def to_hash
-
@flashes.dup
-
end
-
-
1
def empty?
-
@flashes.empty?
-
end
-
-
1
def clear
-
@discard.clear
-
@flashes.clear
-
end
-
-
1
def each(&block)
-
@flashes.each(&block)
-
end
-
-
1
alias :merge! :update
-
-
1
def replace(h) #:nodoc:
-
@discard.clear
-
@flashes.replace h.stringify_keys
-
self
-
end
-
-
# Sets a flash that will not be available to the next action, only to the current.
-
#
-
# flash.now[:message] = "Hello current action"
-
#
-
# This method enables you to use the flash as a central messaging system in your app.
-
# When you need to pass an object to the next action, you use the standard flash assign (<tt>[]=</tt>).
-
# When you need to pass an object to the current action, you use <tt>now</tt>, and your object will
-
# vanish when the current action is done.
-
#
-
# Entries set via <tt>now</tt> are accessed the same way as standard entries: <tt>flash['my-key']</tt>.
-
#
-
# Also, brings two convenience accessors:
-
#
-
# flash.now.alert = "Beware now!"
-
# # Equivalent to flash.now[:alert] = "Beware now!"
-
#
-
# flash.now.notice = "Good luck now!"
-
# # Equivalent to flash.now[:notice] = "Good luck now!"
-
1
def now
-
@now ||= FlashNow.new(self)
-
end
-
-
# Keeps either the entire current flash or a specific flash entry available for the next action:
-
#
-
# flash.keep # keeps the entire flash
-
# flash.keep(:notice) # keeps only the "notice" entry, the rest of the flash is discarded
-
1
def keep(k = nil)
-
k = k.to_s if k
-
@discard.subtract Array(k || keys)
-
k ? self[k] : self
-
end
-
-
# Marks the entire flash or a single flash entry to be discarded by the end of the current action:
-
#
-
# flash.discard # discard the entire flash at the end of the current action
-
# flash.discard(:warning) # discard only the "warning" entry at the end of the current action
-
1
def discard(k = nil)
-
k = k.to_s if k
-
@discard.merge Array(k || keys)
-
k ? self[k] : self
-
end
-
-
# Mark for removal entries that were kept, and delete unkept ones.
-
#
-
# This method is called automatically by filters, so you generally don't need to care about it.
-
1
def sweep #:nodoc:
-
@discard.each { |k| @flashes.delete k }
-
@discard.replace @flashes.keys
-
end
-
-
# Convenience accessor for <tt>flash[:alert]</tt>.
-
1
def alert
-
self[:alert]
-
end
-
-
# Convenience accessor for <tt>flash[:alert]=</tt>.
-
1
def alert=(message)
-
self[:alert] = message
-
end
-
-
# Convenience accessor for <tt>flash[:notice]</tt>.
-
1
def notice
-
self[:notice]
-
end
-
-
# Convenience accessor for <tt>flash[:notice]=</tt>.
-
1
def notice=(message)
-
self[:notice] = message
-
end
-
-
1
protected
-
1
def now_is_loaded?
-
@now
-
end
-
-
1
private
-
1
def stringify_array(array) # :doc:
-
array.map do |item|
-
item.kind_of?(Symbol) ? item.to_s : item
-
end
-
end
-
end
-
-
2
def self.new(app) app; end
-
end
-
-
1
class Request
-
1
prepend Flash::RequestMethods
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActionDispatch
-
# When called, this middleware renders an error page. By default if an HTML
-
# response is expected it will render static error pages from the <tt>/public</tt>
-
# directory. For example when this middleware receives a 500 response it will
-
# render the template found in <tt>/public/500.html</tt>.
-
# If an internationalized locale is set, this middleware will attempt to render
-
# the template in <tt>/public/500.<locale>.html</tt>. If an internationalized template
-
# is not found it will fall back on <tt>/public/500.html</tt>.
-
#
-
# When a request with a content type other than HTML is made, this middleware
-
# will attempt to convert error information into the appropriate response type.
-
1
class PublicExceptions
-
1
attr_accessor :public_path
-
-
1
def initialize(public_path)
-
1
@public_path = public_path
-
end
-
-
1
def call(env)
-
request = ActionDispatch::Request.new(env)
-
status = request.path_info[1..-1].to_i
-
content_type = request.formats.first
-
body = { status: status, error: Rack::Utils::HTTP_STATUS_CODES.fetch(status, Rack::Utils::HTTP_STATUS_CODES[500]) }
-
-
render(status, content_type, body)
-
end
-
-
1
private
-
-
1
def render(status, content_type, body)
-
format = "to_#{content_type.to_sym}" if content_type
-
if format && body.respond_to?(format)
-
render_format(status, content_type, body.public_send(format))
-
else
-
render_html(status)
-
end
-
end
-
-
1
def render_format(status, content_type, body)
-
[status, { "Content-Type" => "#{content_type}; charset=#{ActionDispatch::Response.default_charset}",
-
"Content-Length" => body.bytesize.to_s }, [body]]
-
end
-
-
1
def render_html(status)
-
path = "#{public_path}/#{status}.#{I18n.locale}.html"
-
path = "#{public_path}/#{status}.html" unless (found = File.exist?(path))
-
-
if found || File.exist?(path)
-
render_format(status, "text/html", File.read(path))
-
else
-
[404, { "X-Cascade" => "pass" }, []]
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActionDispatch
-
# ActionDispatch::Reloader wraps the request with callbacks provided by ActiveSupport::Reloader
-
# callbacks, intended to assist with code reloading during development.
-
#
-
# By default, ActionDispatch::Reloader is included in the middleware stack
-
# only in the development environment; specifically, when +config.cache_classes+
-
# is false.
-
1
class Reloader < Executor
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "ipaddr"
-
-
1
module ActionDispatch
-
# This middleware calculates the IP address of the remote client that is
-
# making the request. It does this by checking various headers that could
-
# contain the address, and then picking the last-set address that is not
-
# on the list of trusted IPs. This follows the precedent set by e.g.
-
# {the Tomcat server}[https://issues.apache.org/bugzilla/show_bug.cgi?id=50453],
-
# with {reasoning explained at length}[http://blog.gingerlime.com/2012/rails-ip-spoofing-vulnerabilities-and-protection]
-
# by @gingerlime. A more detailed explanation of the algorithm is given
-
# at GetIp#calculate_ip.
-
#
-
# Some Rack servers concatenate repeated headers, like {HTTP RFC 2616}[https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2]
-
# requires. Some Rack servers simply drop preceding headers, and only report
-
# the value that was {given in the last header}[http://andre.arko.net/2011/12/26/repeated-headers-and-ruby-web-servers].
-
# If you are behind multiple proxy servers (like NGINX to HAProxy to Unicorn)
-
# then you should test your Rack server to make sure your data is good.
-
#
-
# IF YOU DON'T USE A PROXY, THIS MAKES YOU VULNERABLE TO IP SPOOFING.
-
# This middleware assumes that there is at least one proxy sitting around
-
# and setting headers with the client's remote IP address. If you don't use
-
# a proxy, because you are hosted on e.g. Heroku without SSL, any client can
-
# claim to have any IP address by setting the X-Forwarded-For header. If you
-
# care about that, then you need to explicitly drop or ignore those headers
-
# sometime before this middleware runs.
-
1
class RemoteIp
-
1
class IpSpoofAttackError < StandardError; end
-
-
# The default trusted IPs list simply includes IP addresses that are
-
# guaranteed by the IP specification to be private addresses. Those will
-
# not be the ultimate client IP in production, and so are discarded. See
-
# https://en.wikipedia.org/wiki/Private_network for details.
-
TRUSTED_PROXIES = [
-
"127.0.0.1", # localhost IPv4
-
"::1", # localhost IPv6
-
"fc00::/7", # private IPv6 range fc00::/7
-
"10.0.0.0/8", # private IPv4 range 10.x.x.x
-
"172.16.0.0/12", # private IPv4 range 172.16.0.0 .. 172.31.255.255
-
"192.168.0.0/16", # private IPv4 range 192.168.x.x
-
6
].map { |proxy| IPAddr.new(proxy) }
-
-
1
attr_reader :check_ip, :proxies
-
-
# Create a new +RemoteIp+ middleware instance.
-
#
-
# The +ip_spoofing_check+ option is on by default. When on, an exception
-
# is raised if it looks like the client is trying to lie about its own IP
-
# address. It makes sense to turn off this check on sites aimed at non-IP
-
# clients (like WAP devices), or behind proxies that set headers in an
-
# incorrect or confusing way (like AWS ELB).
-
#
-
# The +custom_proxies+ argument can take an Array of string, IPAddr, or
-
# Regexp objects which will be used instead of +TRUSTED_PROXIES+. If a
-
# single string, IPAddr, or Regexp object is provided, it will be used in
-
# addition to +TRUSTED_PROXIES+. Any proxy setup will put the value you
-
# want in the middle (or at the beginning) of the X-Forwarded-For list,
-
# with your proxy servers after it. If your proxies aren't removed, pass
-
# them in via the +custom_proxies+ parameter. That way, the middleware will
-
# ignore those IP addresses, and return the one that you want.
-
1
def initialize(app, ip_spoofing_check = true, custom_proxies = nil)
-
1
@app = app
-
1
@check_ip = ip_spoofing_check
-
1
@proxies = if custom_proxies.blank?
-
1
TRUSTED_PROXIES
-
elsif custom_proxies.respond_to?(:any?)
-
custom_proxies
-
else
-
Array(custom_proxies) + TRUSTED_PROXIES
-
end
-
end
-
-
# Since the IP address may not be needed, we store the object here
-
# without calculating the IP to keep from slowing down the majority of
-
# requests. For those requests that do need to know the IP, the
-
# GetIp#calculate_ip method will calculate the memoized client IP address.
-
1
def call(env)
-
2
req = ActionDispatch::Request.new env
-
2
req.remote_ip = GetIp.new(req, check_ip, proxies)
-
2
@app.call(req.env)
-
end
-
-
# The GetIp class exists as a way to defer processing of the request data
-
# into an actual IP address. If the ActionDispatch::Request#remote_ip method
-
# is called, this class will calculate the value and then memoize it.
-
1
class GetIp
-
1
def initialize(req, check_ip, proxies)
-
2
@req = req
-
2
@check_ip = check_ip
-
2
@proxies = proxies
-
end
-
-
# Sort through the various IP address headers, looking for the IP most
-
# likely to be the address of the actual remote client making this
-
# request.
-
#
-
# REMOTE_ADDR will be correct if the request is made directly against the
-
# Ruby process, on e.g. Heroku. When the request is proxied by another
-
# server like HAProxy or NGINX, the IP address that made the original
-
# request will be put in an X-Forwarded-For header. If there are multiple
-
# proxies, that header may contain a list of IPs. Other proxy services
-
# set the Client-Ip header instead, so we check that too.
-
#
-
# As discussed in {this post about Rails IP Spoofing}[http://blog.gingerlime.com/2012/rails-ip-spoofing-vulnerabilities-and-protection/],
-
# while the first IP in the list is likely to be the "originating" IP,
-
# it could also have been set by the client maliciously.
-
#
-
# In order to find the first address that is (probably) accurate, we
-
# take the list of IPs, remove known and trusted proxies, and then take
-
# the last address left, which was presumably set by one of those proxies.
-
1
def calculate_ip
-
# Set by the Rack web server, this is a single value.
-
2
remote_addr = ips_from(@req.remote_addr).last
-
-
# Could be a CSV list and/or repeated headers that were concatenated.
-
2
client_ips = ips_from(@req.client_ip).reverse
-
2
forwarded_ips = ips_from(@req.x_forwarded_for).reverse
-
-
# +Client-Ip+ and +X-Forwarded-For+ should not, generally, both be set.
-
# If they are both set, it means that either:
-
#
-
# 1) This request passed through two proxies with incompatible IP header
-
# conventions.
-
# 2) The client passed one of +Client-Ip+ or +X-Forwarded-For+
-
# (whichever the proxy servers weren't using) themselves.
-
#
-
# Either way, there is no way for us to determine which header is the
-
# right one after the fact. Since we have no idea, if we are concerned
-
# about IP spoofing we need to give up and explode. (If you're not
-
# concerned about IP spoofing you can turn the +ip_spoofing_check+
-
# option off.)
-
2
should_check_ip = @check_ip && client_ips.last && forwarded_ips.last
-
2
if should_check_ip && !forwarded_ips.include?(client_ips.last)
-
# We don't know which came from the proxy, and which from the user
-
raise IpSpoofAttackError, "IP spoofing attack?! " \
-
"HTTP_CLIENT_IP=#{@req.client_ip.inspect} " \
-
"HTTP_X_FORWARDED_FOR=#{@req.x_forwarded_for.inspect}"
-
end
-
-
# We assume these things about the IP headers:
-
#
-
# - X-Forwarded-For will be a list of IPs, one per proxy, or blank
-
# - Client-Ip is propagated from the outermost proxy, or is blank
-
# - REMOTE_ADDR will be the IP that made the request to Rack
-
2
ips = [forwarded_ips, client_ips, remote_addr].flatten.compact
-
-
# If every single IP option is in the trusted list, just return REMOTE_ADDR
-
2
filter_proxies(ips).first || remote_addr
-
end
-
-
# Memoizes the value returned by #calculate_ip and returns it for
-
# ActionDispatch::Request to use.
-
1
def to_s
-
2
@ip ||= calculate_ip
-
end
-
-
1
private
-
-
1
def ips_from(header) # :doc:
-
6
return [] unless header
-
# Split the comma-separated list into an array of strings.
-
2
ips = header.strip.split(/[,\s]+/)
-
2
ips.select do |ip|
-
2
begin
-
# Only return IPs that are valid according to the IPAddr#new method.
-
2
range = IPAddr.new(ip).to_range
-
# We want to make sure nobody is sneaking a netmask in.
-
2
range.begin == range.end
-
rescue ArgumentError
-
nil
-
end
-
end
-
end
-
-
1
def filter_proxies(ips) # :doc:
-
2
ips.reject do |ip|
-
4
@proxies.any? { |proxy| proxy === ip }
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "securerandom"
-
1
require "active_support/core_ext/string/access"
-
-
1
module ActionDispatch
-
# Makes a unique request id available to the +action_dispatch.request_id+ env variable (which is then accessible
-
# through <tt>ActionDispatch::Request#request_id</tt> or the alias <tt>ActionDispatch::Request#uuid</tt>) and sends
-
# the same id to the client via the X-Request-Id header.
-
#
-
# The unique request id is either based on the X-Request-Id header in the request, which would typically be generated
-
# by a firewall, load balancer, or the web server, or, if this header is not available, a random uuid. If the
-
# header is accepted from the outside world, we sanitize it to a max of 255 chars and alphanumeric and dashes only.
-
#
-
# The unique request id can be used to trace a request end-to-end and would typically end up being part of log files
-
# from multiple pieces of the stack.
-
1
class RequestId
-
1
X_REQUEST_ID = "X-Request-Id".freeze #:nodoc:
-
-
1
def initialize(app)
-
1
@app = app
-
end
-
-
1
def call(env)
-
2
req = ActionDispatch::Request.new env
-
2
req.request_id = make_request_id(req.x_request_id)
-
4
@app.call(env).tap { |_status, headers, _body| headers[X_REQUEST_ID] = req.request_id }
-
end
-
-
1
private
-
1
def make_request_id(request_id)
-
2
if request_id.presence
-
request_id.gsub(/[^\w\-@]/, "".freeze).first(255)
-
else
-
2
internal_request_id
-
end
-
end
-
-
1
def internal_request_id
-
2
SecureRandom.uuid
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "rack/utils"
-
1
require "rack/request"
-
1
require "rack/session/abstract/id"
-
1
require "action_dispatch/middleware/cookies"
-
1
require "action_dispatch/request/session"
-
-
1
module ActionDispatch
-
1
module Session
-
1
class SessionRestoreError < StandardError #:nodoc:
-
1
def initialize
-
super("Session contains objects whose class definition isn't available.\n" \
-
"Remember to require the classes for all objects kept in the session.\n" \
-
"(Original exception: #{$!.message} [#{$!.class}])\n")
-
set_backtrace $!.backtrace
-
end
-
end
-
-
1
module Compatibility
-
1
def initialize(app, options = {})
-
1
options[:key] ||= "_session_id"
-
1
super
-
end
-
-
1
def generate_sid
-
sid = SecureRandom.hex(16)
-
sid.encode!(Encoding::UTF_8)
-
sid
-
end
-
-
1
private
-
-
1
def initialize_sid # :doc:
-
1
@default_options.delete(:sidbits)
-
1
@default_options.delete(:secure_random)
-
end
-
-
1
def make_request(env)
-
2
ActionDispatch::Request.new env
-
end
-
end
-
-
1
module StaleSessionCheck
-
1
def load_session(env)
-
stale_session_check! { super }
-
end
-
-
1
def extract_session_id(env)
-
stale_session_check! { super }
-
end
-
-
1
def stale_session_check!
-
yield
-
rescue ArgumentError => argument_error
-
if argument_error.message =~ %r{undefined class/module ([\w:]*\w)}
-
begin
-
# Note that the regexp does not allow $1 to end with a ':'.
-
$1.constantize
-
rescue LoadError, NameError
-
raise ActionDispatch::Session::SessionRestoreError
-
end
-
retry
-
else
-
raise
-
end
-
end
-
end
-
-
1
module SessionObject # :nodoc:
-
1
def prepare_session(req)
-
2
Request::Session.create(self, req, @default_options)
-
end
-
-
1
def loaded_session?(session)
-
2
!session.is_a?(Request::Session) || session.loaded?
-
end
-
end
-
-
1
class AbstractStore < Rack::Session::Abstract::Persisted
-
1
include Compatibility
-
1
include StaleSessionCheck
-
1
include SessionObject
-
-
1
private
-
-
1
def set_cookie(request, session_id, cookie)
-
request.cookie_jar[key] = cookie
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_support/core_ext/hash/keys"
-
1
require "action_dispatch/middleware/session/abstract_store"
-
1
require "rack/session/cookie"
-
-
1
module ActionDispatch
-
1
module Session
-
# This cookie-based session store is the Rails default. It is
-
# dramatically faster than the alternatives.
-
#
-
# Sessions typically contain at most a user_id and flash message; both fit
-
# within the 4K cookie size limit. A CookieOverflow exception is raised if
-
# you attempt to store more than 4K of data.
-
#
-
# The cookie jar used for storage is automatically configured to be the
-
# best possible option given your application's configuration.
-
#
-
# If you only have secret_token set, your cookies will be signed, but
-
# not encrypted. This means a user cannot alter their +user_id+ without
-
# knowing your app's secret key, but can easily read their +user_id+. This
-
# was the default for Rails 3 apps.
-
#
-
# Your cookies will be encrypted using your apps secret_key_base. This
-
# goes a step further than signed cookies in that encrypted cookies cannot
-
# be altered or read by users. This is the default starting in Rails 4.
-
#
-
# Configure your session store in <tt>config/initializers/session_store.rb</tt>:
-
#
-
# Rails.application.config.session_store :cookie_store, key: '_your_app_session'
-
#
-
# In the development and test environments your application's secret key base is
-
# generated by Rails and stored in a temporary file in <tt>tmp/development_secret.txt</tt>.
-
# In all other environments, it is stored encrypted in the
-
# <tt>config/credentials.yml.enc</tt> file.
-
#
-
# If your application was not updated to Rails 5.2 defaults, the secret_key_base
-
# will be found in the old <tt>config/secrets.yml</tt> file.
-
#
-
# Note that changing your secret_key_base will invalidate all existing session.
-
# Additionally, you should take care to make sure you are not relying on the
-
# ability to decode signed cookies generated by your app in external
-
# applications or JavaScript before changing it.
-
#
-
# Because CookieStore extends Rack::Session::Abstract::Persisted, many of the
-
# options described there can be used to customize the session cookie that
-
# is generated. For example:
-
#
-
# Rails.application.config.session_store :cookie_store, expire_after: 14.days
-
#
-
# would set the session cookie to expire automatically 14 days after creation.
-
# Other useful options include <tt>:key</tt>, <tt>:secure</tt> and
-
# <tt>:httponly</tt>.
-
1
class CookieStore < AbstractStore
-
1
def initialize(app, options = {})
-
1
super(app, options.merge!(cookie_only: true))
-
end
-
-
1
def delete_session(req, session_id, options)
-
new_sid = generate_sid unless options[:drop]
-
# Reset hash and Assign the new session id
-
req.set_header("action_dispatch.request.unsigned_session_cookie", new_sid ? { "session_id" => new_sid } : {})
-
new_sid
-
end
-
-
1
def load_session(req)
-
stale_session_check! do
-
data = unpacked_cookie_data(req)
-
data = persistent_session_id!(data)
-
[data["session_id"], data]
-
end
-
end
-
-
1
private
-
-
1
def extract_session_id(req)
-
stale_session_check! do
-
unpacked_cookie_data(req)["session_id"]
-
end
-
end
-
-
1
def unpacked_cookie_data(req)
-
req.fetch_header("action_dispatch.request.unsigned_session_cookie") do |k|
-
v = stale_session_check! do
-
if data = get_cookie(req)
-
data.stringify_keys!
-
end
-
data || {}
-
end
-
req.set_header k, v
-
end
-
end
-
-
1
def persistent_session_id!(data, sid = nil)
-
data ||= {}
-
data["session_id"] ||= sid || generate_sid
-
data
-
end
-
-
1
def write_session(req, sid, session_data, options)
-
session_data["session_id"] = sid
-
session_data
-
end
-
-
1
def set_cookie(request, session_id, cookie)
-
cookie_jar(request)[@key] = cookie
-
end
-
-
1
def get_cookie(req)
-
cookie_jar(req)[@key]
-
end
-
-
1
def cookie_jar(request)
-
request.cookie_jar.signed_or_encrypted
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "action_dispatch/http/request"
-
1
require "action_dispatch/middleware/exception_wrapper"
-
-
1
module ActionDispatch
-
# This middleware rescues any exception returned by the application
-
# and calls an exceptions app that will wrap it in a format for the end user.
-
#
-
# The exceptions app should be passed as parameter on initialization
-
# of ShowExceptions. Every time there is an exception, ShowExceptions will
-
# store the exception in env["action_dispatch.exception"], rewrite the
-
# PATH_INFO to the exception status code and call the Rack app.
-
#
-
# If the application returns a "X-Cascade" pass response, this middleware
-
# will send an empty response as result with the correct status code.
-
# If any exception happens inside the exceptions app, this middleware
-
# catches the exceptions and returns a FAILSAFE_RESPONSE.
-
1
class ShowExceptions
-
1
FAILSAFE_RESPONSE = [500, { "Content-Type" => "text/plain" },
-
["500 Internal Server Error\n" \
-
"If you are the administrator of this website, then please read this web " \
-
"application's log file and/or the web server's log file to find out what " \
-
"went wrong."]]
-
-
1
def initialize(app, exceptions_app)
-
1
@app = app
-
1
@exceptions_app = exceptions_app
-
end
-
-
1
def call(env)
-
2
request = ActionDispatch::Request.new env
-
2
@app.call(env)
-
rescue Exception => exception
-
if request.show_exceptions?
-
render_exception(request, exception)
-
else
-
raise exception
-
end
-
end
-
-
1
private
-
-
1
def render_exception(request, exception)
-
backtrace_cleaner = request.get_header "action_dispatch.backtrace_cleaner"
-
wrapper = ExceptionWrapper.new(backtrace_cleaner, exception)
-
status = wrapper.status_code
-
request.set_header "action_dispatch.exception", wrapper.exception
-
request.set_header "action_dispatch.original_path", request.path_info
-
request.path_info = "/#{status}"
-
response = @exceptions_app.call(request.env)
-
response[1]["X-Cascade"] == "pass" ? pass_response(status) : response
-
rescue Exception => failsafe_error
-
$stderr.puts "Error during failsafe response: #{failsafe_error}\n #{failsafe_error.backtrace * "\n "}"
-
FAILSAFE_RESPONSE
-
end
-
-
1
def pass_response(status)
-
[status, { "Content-Type" => "text/html; charset=#{Response.default_charset}", "Content-Length" => "0" }, []]
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "rack/utils"
-
1
require "active_support/core_ext/uri"
-
-
1
module ActionDispatch
-
# This middleware returns a file's contents from disk in the body response.
-
# When initialized, it can accept optional HTTP headers, which will be set
-
# when a response containing a file's contents is delivered.
-
#
-
# This middleware will render the file specified in <tt>env["PATH_INFO"]</tt>
-
# where the base path is in the +root+ directory. For example, if the +root+
-
# is set to +public/+, then a request with <tt>env["PATH_INFO"]</tt> of
-
# +assets/application.js+ will return a response with the contents of a file
-
# located at +public/assets/application.js+ if the file exists. If the file
-
# does not exist, a 404 "File not Found" response will be returned.
-
1
class FileHandler
-
1
def initialize(root, index: "index", headers: {})
-
1
@root = root.chomp("/").b
-
1
@file_server = ::Rack::File.new(@root, headers)
-
1
@index = index
-
end
-
-
# Takes a path to a file. If the file is found, has valid encoding, and has
-
# correct read permissions, the return value is a URI-escaped string
-
# representing the filename. Otherwise, false is returned.
-
#
-
# Used by the +Static+ class to check the existence of a valid file
-
# in the server's +public/+ directory (see Static#call).
-
1
def match?(path)
-
2
path = ::Rack::Utils.unescape_path path
-
2
return false unless ::Rack::Utils.valid_path? path
-
2
path = ::Rack::Utils.clean_path_info path
-
-
2
paths = [path, "#{path}#{ext}", "#{path}/#{@index}#{ext}"]
-
-
2
if match = paths.detect { |p|
-
6
path = File.join(@root, p.b)
-
6
begin
-
6
File.file?(path) && File.readable?(path)
-
rescue SystemCallError
-
false
-
end
-
-
}
-
return ::Rack::Utils.escape_path(match).b
-
end
-
end
-
-
1
def call(env)
-
serve(Rack::Request.new(env))
-
end
-
-
1
def serve(request)
-
path = request.path_info
-
gzip_path = gzip_file_path(path)
-
-
if gzip_path && gzip_encoding_accepted?(request)
-
request.path_info = gzip_path
-
status, headers, body = @file_server.call(request.env)
-
if status == 304
-
return [status, headers, body]
-
end
-
headers["Content-Encoding"] = "gzip"
-
headers["Content-Type"] = content_type(path)
-
else
-
status, headers, body = @file_server.call(request.env)
-
end
-
-
headers["Vary"] = "Accept-Encoding" if gzip_path
-
-
return [status, headers, body]
-
ensure
-
request.path_info = path
-
end
-
-
1
private
-
1
def ext
-
4
::ActionController::Base.default_static_extension
-
end
-
-
1
def content_type(path)
-
::Rack::Mime.mime_type(::File.extname(path), "text/plain".freeze)
-
end
-
-
1
def gzip_encoding_accepted?(request)
-
request.accept_encoding.any? { |enc, quality| enc =~ /\bgzip\b/i }
-
end
-
-
1
def gzip_file_path(path)
-
can_gzip_mime = content_type(path) =~ /\A(?:text\/|application\/javascript)/
-
gzip_path = "#{path}.gz"
-
if can_gzip_mime && File.exist?(File.join(@root, ::Rack::Utils.unescape_path(gzip_path).b))
-
gzip_path.b
-
else
-
false
-
end
-
end
-
end
-
-
# This middleware will attempt to return the contents of a file's body from
-
# disk in the response. If a file is not found on disk, the request will be
-
# delegated to the application stack. This middleware is commonly initialized
-
# to serve assets from a server's +public/+ directory.
-
#
-
# This middleware verifies the path to ensure that only files
-
# living in the root directory can be rendered. A request cannot
-
# produce a directory traversal using this middleware. Only 'GET' and 'HEAD'
-
# requests will result in a file being returned.
-
1
class Static
-
1
def initialize(app, path, index: "index", headers: {})
-
1
@app = app
-
1
@file_handler = FileHandler.new(path, index: index, headers: headers)
-
end
-
-
1
def call(env)
-
2
req = Rack::Request.new env
-
-
2
if req.get? || req.head?
-
2
path = req.path_info.chomp("/".freeze)
-
2
if match = @file_handler.match?(path)
-
req.path_info = match
-
return @file_handler.serve(req)
-
end
-
end
-
-
2
@app.call(req.env)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "rack/session/abstract/id"
-
-
1
module ActionDispatch
-
1
class Request
-
# Session is responsible for lazily loading the session from store.
-
1
class Session # :nodoc:
-
1
ENV_SESSION_KEY = Rack::RACK_SESSION # :nodoc:
-
1
ENV_SESSION_OPTIONS_KEY = Rack::RACK_SESSION_OPTIONS # :nodoc:
-
-
# Singleton object used to determine if an optional param wasn't specified.
-
1
Unspecified = Object.new
-
-
# Creates a session hash, merging the properties of the previous session if any.
-
1
def self.create(store, req, default_options)
-
2
session_was = find req
-
2
session = Request::Session.new(store, req)
-
2
session.merge! session_was if session_was
-
-
2
set(req, session)
-
2
Options.set(req, Request::Session::Options.new(store, default_options))
-
2
session
-
end
-
-
1
def self.find(req)
-
2
req.get_header ENV_SESSION_KEY
-
end
-
-
1
def self.set(req, session)
-
2
req.set_header ENV_SESSION_KEY, session
-
end
-
-
1
class Options #:nodoc:
-
1
def self.set(req, options)
-
2
req.set_header ENV_SESSION_OPTIONS_KEY, options
-
end
-
-
1
def self.find(req)
-
2
req.get_header ENV_SESSION_OPTIONS_KEY
-
end
-
-
1
def initialize(by, default_options)
-
2
@by = by
-
2
@delegate = default_options.dup
-
end
-
-
1
def [](key)
-
6
@delegate[key]
-
end
-
-
1
def id(req)
-
@delegate.fetch(:id) {
-
@by.send(:extract_session_id, req)
-
}
-
end
-
-
1
def []=(k, v); @delegate[k] = v; end
-
1
def to_hash; @delegate.dup; end
-
3
def values_at(*args); @delegate.values_at(*args); end
-
end
-
-
1
def initialize(by, req)
-
2
@by = by
-
2
@req = req
-
2
@delegate = {}
-
2
@loaded = false
-
2
@exists = nil # We haven't checked yet.
-
end
-
-
1
def id
-
options.id(@req)
-
end
-
-
1
def options
-
2
Options.find @req
-
end
-
-
1
def destroy
-
clear
-
options = self.options || {}
-
@by.send(:delete_session, @req, options.id(@req), options)
-
-
# Load the new sid to be written with the response.
-
@loaded = false
-
load_for_write!
-
end
-
-
# Returns value of the key stored in the session or
-
# +nil+ if the given key is not found in the session.
-
1
def [](key)
-
load_for_read!
-
@delegate[key.to_s]
-
end
-
-
# Returns true if the session has the given key or false.
-
1
def has_key?(key)
-
load_for_read!
-
@delegate.key?(key.to_s)
-
end
-
1
alias :key? :has_key?
-
1
alias :include? :has_key?
-
-
# Returns keys of the session as Array.
-
1
def keys
-
load_for_read!
-
@delegate.keys
-
end
-
-
# Returns values of the session as Array.
-
1
def values
-
load_for_read!
-
@delegate.values
-
end
-
-
# Writes given value to given key of the session.
-
1
def []=(key, value)
-
load_for_write!
-
@delegate[key.to_s] = value
-
end
-
-
# Clears the session.
-
1
def clear
-
load_for_write!
-
@delegate.clear
-
end
-
-
# Returns the session as Hash.
-
1
def to_hash
-
load_for_read!
-
@delegate.dup.delete_if { |_, v| v.nil? }
-
end
-
1
alias :to_h :to_hash
-
-
# Updates the session with given Hash.
-
#
-
# session.to_hash
-
# # => {"session_id"=>"e29b9ea315edf98aad94cc78c34cc9b2"}
-
#
-
# session.update({ "foo" => "bar" })
-
# # => {"session_id"=>"e29b9ea315edf98aad94cc78c34cc9b2", "foo" => "bar"}
-
#
-
# session.to_hash
-
# # => {"session_id"=>"e29b9ea315edf98aad94cc78c34cc9b2", "foo" => "bar"}
-
1
def update(hash)
-
load_for_write!
-
@delegate.update stringify_keys(hash)
-
end
-
-
# Deletes given key from the session.
-
1
def delete(key)
-
load_for_write!
-
@delegate.delete key.to_s
-
end
-
-
# Returns value of the given key from the session, or raises +KeyError+
-
# if can't find the given key and no default value is set.
-
# Returns default value if specified.
-
#
-
# session.fetch(:foo)
-
# # => KeyError: key not found: "foo"
-
#
-
# session.fetch(:foo, :bar)
-
# # => :bar
-
#
-
# session.fetch(:foo) do
-
# :bar
-
# end
-
# # => :bar
-
1
def fetch(key, default = Unspecified, &block)
-
load_for_read!
-
if default == Unspecified
-
@delegate.fetch(key.to_s, &block)
-
else
-
@delegate.fetch(key.to_s, default, &block)
-
end
-
end
-
-
1
def inspect
-
if loaded?
-
super
-
else
-
"#<#{self.class}:0x#{(object_id << 1).to_s(16)} not yet loaded>"
-
end
-
end
-
-
1
def exists?
-
return @exists unless @exists.nil?
-
@exists = @by.send(:session_exists?, @req)
-
end
-
-
1
def loaded?
-
4
@loaded
-
end
-
-
1
def empty?
-
load_for_read!
-
@delegate.empty?
-
end
-
-
1
def merge!(other)
-
load_for_write!
-
@delegate.merge!(other)
-
end
-
-
1
def each(&block)
-
to_hash.each(&block)
-
end
-
-
1
private
-
-
1
def load_for_read!
-
load! if !loaded? && exists?
-
end
-
-
1
def load_for_write!
-
load! unless loaded?
-
end
-
-
1
def load!
-
id, session = @by.load_session @req
-
options[:id] = id
-
@delegate.replace(stringify_keys(session))
-
@loaded = true
-
end
-
-
1
def stringify_keys(other)
-
other.each_with_object({}) { |(key, value), hash|
-
hash[key.to_s] = value
-
}
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_support/core_ext/hash/indifferent_access"
-
-
1
module ActionDispatch
-
1
class Request
-
1
class Utils # :nodoc:
-
1
mattr_accessor :perform_deep_munge, default: true
-
-
1
def self.each_param_value(params, &block)
-
case params
-
when Array
-
params.each { |element| each_param_value(element, &block) }
-
when Hash
-
params.each_value { |value| each_param_value(value, &block) }
-
when String
-
block.call params
-
end
-
end
-
-
1
def self.normalize_encode_params(params)
-
4
if perform_deep_munge
-
4
NoNilParamEncoder.normalize_encode_params params
-
else
-
ParamEncoder.normalize_encode_params params
-
end
-
end
-
-
1
def self.check_param_encoding(params)
-
8
case params
-
when Array
-
params.each { |element| check_param_encoding(element) }
-
when Hash
-
8
params.each_value { |value| check_param_encoding(value) }
-
when String
-
4
unless params.valid_encoding?
-
# Raise Rack::Utils::InvalidParameterError for consistency with Rack.
-
# ActionDispatch::Request#GET will re-raise as a BadRequest error.
-
raise Rack::Utils::InvalidParameterError, "Invalid encoding for parameter: #{params.scrub}"
-
end
-
end
-
end
-
-
1
class ParamEncoder # :nodoc:
-
# Convert nested Hash to HashWithIndifferentAccess.
-
1
def self.normalize_encode_params(params)
-
4
case params
-
when Array
-
handle_array params
-
when Hash
-
4
if params.has_key?(:tempfile)
-
ActionDispatch::Http::UploadedFile.new(params)
-
else
-
4
params.each_with_object({}) do |(key, val), new_hash|
-
new_hash[key] = normalize_encode_params(val)
-
end.with_indifferent_access
-
end
-
else
-
params
-
end
-
end
-
-
1
def self.handle_array(params)
-
params.map! { |el| normalize_encode_params(el) }
-
end
-
end
-
-
# Remove nils from the params hash.
-
1
class NoNilParamEncoder < ParamEncoder # :nodoc:
-
1
def self.handle_array(params)
-
list = super
-
list.compact!
-
list
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "delegate"
-
1
require "active_support/core_ext/string/strip"
-
-
1
module ActionDispatch
-
1
module Routing
-
1
class RouteWrapper < SimpleDelegator
-
1
def endpoint
-
app.dispatcher? ? "#{controller}##{action}" : rack_app.inspect
-
end
-
-
1
def constraints
-
requirements.except(:controller, :action)
-
end
-
-
1
def rack_app
-
app.rack_app
-
end
-
-
1
def path
-
super.spec.to_s
-
end
-
-
1
def name
-
super.to_s
-
end
-
-
1
def reqs
-
@reqs ||= begin
-
reqs = endpoint
-
reqs += " #{constraints}" unless constraints.empty?
-
reqs
-
end
-
end
-
-
1
def controller
-
parts.include?(:controller) ? ":controller" : requirements[:controller]
-
end
-
-
1
def action
-
parts.include?(:action) ? ":action" : requirements[:action]
-
end
-
-
1
def internal?
-
internal
-
end
-
-
1
def engine?
-
app.engine?
-
end
-
end
-
-
##
-
# This class is just used for displaying route information when someone
-
# executes `rails routes` or looks at the RoutingError page.
-
# People should not use this class.
-
1
class RoutesInspector # :nodoc:
-
1
def initialize(routes)
-
@engines = {}
-
@routes = routes
-
end
-
-
1
def format(formatter, filter = nil)
-
routes_to_display = filter_routes(normalize_filter(filter))
-
routes = collect_routes(routes_to_display)
-
if routes.none?
-
formatter.no_routes(collect_routes(@routes))
-
return formatter.result
-
end
-
-
formatter.header routes
-
formatter.section routes
-
-
@engines.each do |name, engine_routes|
-
formatter.section_title "Routes for #{name}"
-
formatter.section engine_routes
-
end
-
-
formatter.result
-
end
-
-
1
private
-
-
1
def normalize_filter(filter)
-
if filter.is_a?(Hash) && filter[:controller]
-
{ controller: /#{filter[:controller].underscore.sub(/_?controller\z/, "")}/ }
-
elsif filter
-
{ controller: /#{filter}/, action: /#{filter}/, verb: /#{filter}/, name: /#{filter}/, path: /#{filter}/ }
-
end
-
end
-
-
1
def filter_routes(filter)
-
if filter
-
@routes.select do |route|
-
route_wrapper = RouteWrapper.new(route)
-
filter.any? { |default, value| route_wrapper.send(default) =~ value }
-
end
-
else
-
@routes
-
end
-
end
-
-
1
def collect_routes(routes)
-
routes.collect do |route|
-
RouteWrapper.new(route)
-
end.reject(&:internal?).collect do |route|
-
collect_engine_routes(route)
-
-
{ name: route.name,
-
verb: route.verb,
-
path: route.path,
-
reqs: route.reqs }
-
end
-
end
-
-
1
def collect_engine_routes(route)
-
name = route.endpoint
-
return unless route.engine?
-
return if @engines[name]
-
-
routes = route.rack_app.routes
-
if routes.is_a?(ActionDispatch::Routing::RouteSet)
-
@engines[name] = collect_routes(routes.routes)
-
end
-
end
-
end
-
-
1
class ConsoleFormatter
-
1
def initialize
-
@buffer = []
-
end
-
-
1
def result
-
@buffer.join("\n")
-
end
-
-
1
def section_title(title)
-
@buffer << "\n#{title}:"
-
end
-
-
1
def section(routes)
-
@buffer << draw_section(routes)
-
end
-
-
1
def header(routes)
-
@buffer << draw_header(routes)
-
end
-
-
1
def no_routes(routes)
-
@buffer <<
-
if routes.none?
-
<<-MESSAGE.strip_heredoc
-
You don't have any routes defined!
-
-
Please add some routes in config/routes.rb.
-
MESSAGE
-
else
-
"No routes were found for this controller"
-
end
-
@buffer << "For more information about routes, see the Rails guide: http://guides.rubyonrails.org/routing.html."
-
end
-
-
1
private
-
1
def draw_section(routes)
-
header_lengths = ["Prefix", "Verb", "URI Pattern"].map(&:length)
-
name_width, verb_width, path_width = widths(routes).zip(header_lengths).map(&:max)
-
-
routes.map do |r|
-
"#{r[:name].rjust(name_width)} #{r[:verb].ljust(verb_width)} #{r[:path].ljust(path_width)} #{r[:reqs]}"
-
end
-
end
-
-
1
def draw_header(routes)
-
name_width, verb_width, path_width = widths(routes)
-
-
"#{"Prefix".rjust(name_width)} #{"Verb".ljust(verb_width)} #{"URI Pattern".ljust(path_width)} Controller#Action"
-
end
-
-
1
def widths(routes)
-
[routes.map { |r| r[:name].length }.max || 0,
-
routes.map { |r| r[:verb].length }.max || 0,
-
routes.map { |r| r[:path].length }.max || 0]
-
end
-
end
-
-
1
class HtmlTableFormatter
-
1
def initialize(view)
-
@view = view
-
@buffer = []
-
end
-
-
1
def section_title(title)
-
@buffer << %(<tr><th colspan="4">#{title}</th></tr>)
-
end
-
-
1
def section(routes)
-
@buffer << @view.render(partial: "routes/route", collection: routes)
-
end
-
-
# The header is part of the HTML page, so we don't construct it here.
-
1
def header(routes)
-
end
-
-
1
def no_routes(*)
-
@buffer << <<-MESSAGE.strip_heredoc
-
<p>You don't have any routes defined!</p>
-
<ul>
-
<li>Please add some routes in <tt>config/routes.rb</tt>.</li>
-
<li>
-
For more information about routes, please see the Rails guide
-
<a href="http://guides.rubyonrails.org/routing.html">Rails Routing from the Outside In</a>.
-
</li>
-
</ul>
-
MESSAGE
-
end
-
-
1
def result
-
@view.raw @view.render(layout: "routes/table") {
-
@view.raw @buffer.join("\n")
-
}
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_support/core_ext/hash/slice"
-
1
require "active_support/core_ext/enumerable"
-
1
require "active_support/core_ext/array/extract_options"
-
1
require "active_support/core_ext/regexp"
-
1
require "action_dispatch/routing/redirection"
-
1
require "action_dispatch/routing/endpoint"
-
-
1
module ActionDispatch
-
1
module Routing
-
1
class Mapper
-
1
URL_OPTIONS = [:protocol, :subdomain, :domain, :host, :port]
-
-
1
class Constraints < Routing::Endpoint #:nodoc:
-
1
attr_reader :app, :constraints
-
-
1
SERVE = ->(app, req) { app.serve req }
-
1
CALL = ->(app, req) { app.call req.env }
-
-
1
def initialize(app, constraints, strategy)
-
# Unwrap Constraints objects. I don't actually think it's possible
-
# to pass a Constraints object to this constructor, but there were
-
# multiple places that kept testing children of this object. I
-
# *think* they were just being defensive, but I have no idea.
-
2
if app.is_a?(self.class)
-
constraints += app.constraints
-
app = app.app
-
end
-
-
2
@strategy = strategy
-
-
2
@app, @constraints, = app, constraints
-
end
-
-
1
def dispatcher?; @strategy == SERVE; end
-
-
1
def matches?(req)
-
@constraints.all? do |constraint|
-
(constraint.respond_to?(:matches?) && constraint.matches?(req)) ||
-
(constraint.respond_to?(:call) && constraint.call(*constraint_args(constraint, req)))
-
end
-
end
-
-
1
def serve(req)
-
return [ 404, { "X-Cascade" => "pass" }, [] ] unless matches?(req)
-
-
@strategy.call @app, req
-
end
-
-
1
private
-
1
def constraint_args(constraint, request)
-
constraint.arity == 1 ? [request] : [request.path_parameters, request]
-
end
-
end
-
-
1
class Mapping #:nodoc:
-
1
ANCHOR_CHARACTERS_REGEX = %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z}
-
1
OPTIONAL_FORMAT_REGEX = %r{(?:\(\.:format\)+|\.:format|/)\Z}
-
-
1
attr_reader :requirements, :defaults
-
1
attr_reader :to, :default_controller, :default_action
-
1
attr_reader :required_defaults, :ast
-
-
1
def self.build(scope, set, ast, controller, default_action, to, via, formatted, options_constraints, anchor, options)
-
18
options = scope[:options].merge(options) if scope[:options]
-
-
18
defaults = (scope[:defaults] || {}).dup
-
18
scope_constraints = scope[:constraints] || {}
-
-
18
new set, ast, defaults, controller, default_action, scope[:module], to, formatted, scope_constraints, scope[:blocks] || [], via, options_constraints, anchor, options
-
end
-
-
1
def self.check_via(via)
-
18
if via.empty?
-
msg = "You should not use the `match` method in your router without specifying an HTTP method.\n" \
-
"If you want to expose your action to both GET and POST, add `via: [:get, :post]` option.\n" \
-
"If you want to expose your action to GET, use `get` in the router:\n" \
-
" Instead of: match \"controller#action\"\n" \
-
" Do: get \"controller#action\""
-
raise ArgumentError, msg
-
end
-
18
via
-
end
-
-
1
def self.normalize_path(path, format)
-
18
path = Mapper.normalize_path(path)
-
-
18
if format == true
-
"#{path}.:format"
-
17
elsif optional_format?(path, format)
-
14
"#{path}(.:format)"
-
else
-
4
path
-
end
-
end
-
-
1
def self.optional_format?(path, format)
-
18
format != false && path !~ OPTIONAL_FORMAT_REGEX
-
end
-
-
1
def initialize(set, ast, defaults, controller, default_action, modyoule, to, formatted, scope_constraints, blocks, via, options_constraints, anchor, options)
-
18
@defaults = defaults
-
18
@set = set
-
-
18
@to = to
-
18
@default_controller = controller
-
18
@default_action = default_action
-
18
@ast = ast
-
18
@anchor = anchor
-
18
@via = via
-
18
@internal = options.delete(:internal)
-
-
18
path_params = ast.find_all(&:symbol?).map(&:to_sym)
-
-
18
options = add_wildcard_options(options, formatted, ast)
-
-
18
options = normalize_options!(options, path_params, modyoule)
-
-
18
split_options = constraints(options, path_params)
-
-
18
constraints = scope_constraints.merge Hash[split_options[:constraints] || []]
-
-
18
if options_constraints.is_a?(Hash)
-
18
@defaults = Hash[options_constraints.find_all { |key, default|
-
URL_OPTIONS.include?(key) && (String === default || Integer === default)
-
}].merge @defaults
-
18
@blocks = blocks
-
18
constraints.merge! options_constraints
-
else
-
@blocks = blocks(options_constraints)
-
end
-
-
18
requirements, conditions = split_constraints path_params, constraints
-
18
verify_regexp_requirements requirements.map(&:last).grep(Regexp)
-
-
18
formats = normalize_format(formatted)
-
-
18
@requirements = formats[:requirements].merge Hash[requirements]
-
18
@conditions = Hash[conditions]
-
18
@defaults = formats[:defaults].merge(@defaults).merge(normalize_defaults(options))
-
-
18
if path_params.include?(:action) && !@requirements.key?(:action)
-
@defaults[:action] ||= "index"
-
end
-
-
18
@required_defaults = (split_options[:required_defaults] || []).map(&:first)
-
end
-
-
1
def make_route(name, precedence)
-
36
route = Journey::Route.new(name,
-
17
application,
-
17
path,
-
17
conditions,
-
17
required_defaults,
-
17
defaults,
-
17
request_method,
-
precedence,
-
@internal)
-
-
18
route
-
end
-
-
1
def application
-
18
app(@blocks)
-
end
-
-
1
def path
-
18
build_path @ast, requirements, @anchor
-
end
-
-
1
def conditions
-
18
build_conditions @conditions, @set.request_class
-
end
-
-
1
def build_conditions(current_conditions, request_class)
-
18
conditions = current_conditions.dup
-
-
18
conditions.keep_if do |k, _|
-
request_class.public_method_defined?(k)
-
end
-
end
-
1
private :build_conditions
-
-
1
def request_method
-
36
@via.map { |x| Journey::Route.verb_matcher(x) }
-
end
-
1
private :request_method
-
-
1
JOINED_SEPARATORS = SEPARATORS.join # :nodoc:
-
-
1
def build_path(ast, requirements, anchor)
-
18
pattern = Journey::Path::Pattern.new(ast, requirements, JOINED_SEPARATORS, anchor)
-
-
# Find all the symbol nodes that are adjacent to literal nodes and alter
-
# the regexp so that Journey will partition them into custom routes.
-
18
ast.find_all { |node|
-
228
next unless node.cat?
-
-
96
if node.left.literal? && node.right.symbol?
-
symbol = node.right
-
95
elsif node.left.literal? && node.right.cat? && node.right.left.symbol?
-
symbol = node.right.left
-
95
elsif node.left.symbol? && node.right.literal?
-
symbol = node.left
-
95
elsif node.left.symbol? && node.right.cat? && node.right.left.literal?
-
symbol = node.left
-
else
-
96
next
-
end
-
-
if symbol
-
symbol.regexp = /(?:#{Regexp.union(symbol.regexp, '-')})+/
-
end
-
}
-
-
18
pattern
-
end
-
1
private :build_path
-
-
1
private
-
1
def add_wildcard_options(options, formatted, path_ast)
-
# Add a constraint for wildcard route to make it non-greedy and match the
-
# optional format part of the route by default.
-
18
if formatted != false
-
16
path_ast.grep(Journey::Nodes::Star).each_with_object({}) { |node, hash|
-
4
hash[node.name.to_sym] ||= /.+?/
-
}.merge options
-
else
-
2
options
-
end
-
end
-
-
1
def normalize_options!(options, path_params, modyoule)
-
18
if path_params.include?(:controller)
-
raise ArgumentError, ":controller segment is not allowed within a namespace block" if modyoule
-
-
# Add a default constraint for :controller path segments that matches namespaced
-
# controllers with default routes like :controller/:action/:id(.:format), e.g:
-
# GET /admin/products/show/1
-
# => { controller: 'admin/products', action: 'show', id: '1' }
-
options[:controller] ||= /.+?/
-
end
-
-
18
if to.respond_to?(:action) || to.respond_to?(:call)
-
2
options
-
else
-
16
to_endpoint = split_to to
-
16
controller = to_endpoint[0] || default_controller
-
16
action = to_endpoint[1] || default_action
-
-
16
controller = add_controller_module(controller, modyoule)
-
-
16
options.merge! check_controller_and_action(path_params, controller, action)
-
end
-
end
-
-
1
def split_constraints(path_params, constraints)
-
18
constraints.partition do |key, requirement|
-
4
path_params.include?(key) || key == :controller
-
end
-
end
-
-
1
def normalize_format(formatted)
-
18
case formatted
-
when true
-
{ requirements: { format: /.+/ },
-
defaults: {} }
-
when Regexp
-
{ requirements: { format: formatted },
-
defaults: { format: nil } }
-
when String
-
{ requirements: { format: Regexp.compile(formatted) },
-
defaults: { format: formatted } }
-
else
-
18
{ requirements: {}, defaults: {} }
-
end
-
end
-
-
1
def verify_regexp_requirements(requirements)
-
18
requirements.each do |requirement|
-
4
if requirement.source =~ ANCHOR_CHARACTERS_REGEX
-
raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}"
-
end
-
-
4
if requirement.multiline?
-
raise ArgumentError, "Regexp multiline option is not allowed in routing requirements: #{requirement.inspect}"
-
end
-
end
-
end
-
-
1
def normalize_defaults(options)
-
54
Hash[options.reject { |_, default| Regexp === default }]
-
end
-
-
1
def app(blocks)
-
18
if to.respond_to?(:action)
-
Routing::RouteSet::StaticDispatcher.new to
-
17
elsif to.respond_to?(:call)
-
2
Constraints.new(to, blocks, Constraints::CALL)
-
15
elsif blocks.any?
-
Constraints.new(dispatcher(defaults.key?(:controller)), blocks, Constraints::SERVE)
-
else
-
16
dispatcher(defaults.key?(:controller))
-
end
-
end
-
-
1
def check_controller_and_action(path_params, controller, action)
-
16
hash = check_part(:controller, controller, path_params, {}) do |part|
-
16
translate_controller(part) {
-
message = "'#{part}' is not a supported controller name. This can lead to potential routing problems.".dup
-
message << " See http://guides.rubyonrails.org/routing.html#specifying-a-controller-to-use"
-
-
raise ArgumentError, message
-
}
-
end
-
-
16
check_part(:action, action, path_params, hash) { |part|
-
16
part.is_a?(Regexp) ? part : part.to_s
-
}
-
end
-
-
1
def check_part(name, part, path_params, hash)
-
32
if part
-
32
hash[name] = yield(part)
-
else
-
unless path_params.include?(name)
-
message = "Missing :#{name} key on routes definition, please check your routes."
-
raise ArgumentError, message
-
end
-
end
-
32
hash
-
end
-
-
1
def split_to(to)
-
16
if to =~ /#/
-
16
to.split("#")
-
else
-
[]
-
end
-
end
-
-
1
def add_controller_module(controller, modyoule)
-
16
if modyoule && !controller.is_a?(Regexp)
-
if controller =~ %r{\A/}
-
controller[1..-1]
-
else
-
[modyoule, controller].compact.join("/")
-
end
-
else
-
16
controller
-
end
-
end
-
-
1
def translate_controller(controller)
-
16
return controller if Regexp === controller
-
16
return controller.to_s if controller =~ /\A[a-z_0-9][a-z_0-9\/]*\z/
-
-
yield
-
end
-
-
1
def blocks(callable_constraint)
-
unless callable_constraint.respond_to?(:call) || callable_constraint.respond_to?(:matches?)
-
raise ArgumentError, "Invalid constraint: #{callable_constraint.inspect} must respond to :call or :matches?"
-
end
-
[callable_constraint]
-
end
-
-
1
def constraints(options, path_params)
-
18
options.group_by do |key, option|
-
36
if Regexp === option
-
4
:constraints
-
else
-
32
if path_params.include?(key)
-
:path_params
-
else
-
32
:required_defaults
-
end
-
end
-
end
-
end
-
-
1
def dispatcher(raise_on_name_error)
-
16
Routing::RouteSet::Dispatcher.new raise_on_name_error
-
end
-
end
-
-
# Invokes Journey::Router::Utils.normalize_path and ensure that
-
# (:locale) becomes (/:locale) instead of /(:locale). Except
-
# for root cases, where the latter is the correct one.
-
1
def self.normalize_path(path)
-
35
path = Journey::Router::Utils.normalize_path(path)
-
35
path.gsub!(%r{/(\(+)/?}, '\1/') unless path =~ %r{^/(\(+[^)]+\)){1,}$}
-
35
path
-
end
-
-
1
def self.normalize_name(name)
-
17
normalize_path(name)[1..-1].tr("/", "_")
-
end
-
-
1
module Base
-
# Matches a URL pattern to one or more routes.
-
#
-
# You should not use the +match+ method in your router
-
# without specifying an HTTP method.
-
#
-
# If you want to expose your action to both GET and POST, use:
-
#
-
# # sets :controller, :action and :id in params
-
# match ':controller/:action/:id', via: [:get, :post]
-
#
-
# Note that +:controller+, +:action+ and +:id+ are interpreted as URL
-
# query parameters and thus available through +params+ in an action.
-
#
-
# If you want to expose your action to GET, use +get+ in the router:
-
#
-
# Instead of:
-
#
-
# match ":controller/:action/:id"
-
#
-
# Do:
-
#
-
# get ":controller/:action/:id"
-
#
-
# Two of these symbols are special, +:controller+ maps to the controller
-
# and +:action+ to the controller's action. A pattern can also map
-
# wildcard segments (globs) to params:
-
#
-
# get 'songs/*category/:title', to: 'songs#show'
-
#
-
# # 'songs/rock/classic/stairway-to-heaven' sets
-
# # params[:category] = 'rock/classic'
-
# # params[:title] = 'stairway-to-heaven'
-
#
-
# To match a wildcard parameter, it must have a name assigned to it.
-
# Without a variable name to attach the glob parameter to, the route
-
# can't be parsed.
-
#
-
# When a pattern points to an internal route, the route's +:action+ and
-
# +:controller+ should be set in options or hash shorthand. Examples:
-
#
-
# match 'photos/:id' => 'photos#show', via: :get
-
# match 'photos/:id', to: 'photos#show', via: :get
-
# match 'photos/:id', controller: 'photos', action: 'show', via: :get
-
#
-
# A pattern can also point to a +Rack+ endpoint i.e. anything that
-
# responds to +call+:
-
#
-
# match 'photos/:id', to: -> (hash) { [200, {}, ["Coming soon"]] }, via: :get
-
# match 'photos/:id', to: PhotoRackApp, via: :get
-
# # Yes, controller actions are just rack endpoints
-
# match 'photos/:id', to: PhotosController.action(:show), via: :get
-
#
-
# Because requesting various HTTP verbs with a single action has security
-
# implications, you must either specify the actions in
-
# the via options or use one of the HttpHelpers[rdoc-ref:HttpHelpers]
-
# instead +match+
-
#
-
# === Options
-
#
-
# Any options not seen here are passed on as params with the URL.
-
#
-
# [:controller]
-
# The route's controller.
-
#
-
# [:action]
-
# The route's action.
-
#
-
# [:param]
-
# Overrides the default resource identifier +:id+ (name of the
-
# dynamic segment used to generate the routes).
-
# You can access that segment from your controller using
-
# <tt>params[<:param>]</tt>.
-
# In your router:
-
#
-
# resources :users, param: :name
-
#
-
# The +users+ resource here will have the following routes generated for it:
-
#
-
# GET /users(.:format)
-
# POST /users(.:format)
-
# GET /users/new(.:format)
-
# GET /users/:name/edit(.:format)
-
# GET /users/:name(.:format)
-
# PATCH/PUT /users/:name(.:format)
-
# DELETE /users/:name(.:format)
-
#
-
# You can override <tt>ActiveRecord::Base#to_param</tt> of a related
-
# model to construct a URL:
-
#
-
# class User < ActiveRecord::Base
-
# def to_param
-
# name
-
# end
-
# end
-
#
-
# user = User.find_by(name: 'Phusion')
-
# user_path(user) # => "/users/Phusion"
-
#
-
# [:path]
-
# The path prefix for the routes.
-
#
-
# [:module]
-
# The namespace for :controller.
-
#
-
# match 'path', to: 'c#a', module: 'sekret', controller: 'posts', via: :get
-
# # => Sekret::PostsController
-
#
-
# See <tt>Scoping#namespace</tt> for its scope equivalent.
-
#
-
# [:as]
-
# The name used to generate routing helpers.
-
#
-
# [:via]
-
# Allowed HTTP verb(s) for route.
-
#
-
# match 'path', to: 'c#a', via: :get
-
# match 'path', to: 'c#a', via: [:get, :post]
-
# match 'path', to: 'c#a', via: :all
-
#
-
# [:to]
-
# Points to a +Rack+ endpoint. Can be an object that responds to
-
# +call+ or a string representing a controller's action.
-
#
-
# match 'path', to: 'controller#action', via: :get
-
# match 'path', to: -> (env) { [200, {}, ["Success!"]] }, via: :get
-
# match 'path', to: RackApp, via: :get
-
#
-
# [:on]
-
# Shorthand for wrapping routes in a specific RESTful context. Valid
-
# values are +:member+, +:collection+, and +:new+. Only use within
-
# <tt>resource(s)</tt> block. For example:
-
#
-
# resource :bar do
-
# match 'foo', to: 'c#a', on: :member, via: [:get, :post]
-
# end
-
#
-
# Is equivalent to:
-
#
-
# resource :bar do
-
# member do
-
# match 'foo', to: 'c#a', via: [:get, :post]
-
# end
-
# end
-
#
-
# [:constraints]
-
# Constrains parameters with a hash of regular expressions
-
# or an object that responds to <tt>matches?</tt>. In addition, constraints
-
# other than path can also be specified with any object
-
# that responds to <tt>===</tt> (eg. String, Array, Range, etc.).
-
#
-
# match 'path/:id', constraints: { id: /[A-Z]\d{5}/ }, via: :get
-
#
-
# match 'json_only', constraints: { format: 'json' }, via: :get
-
#
-
# class Whitelist
-
# def matches?(request) request.remote_ip == '1.2.3.4' end
-
# end
-
# match 'path', to: 'c#a', constraints: Whitelist.new, via: :get
-
#
-
# See <tt>Scoping#constraints</tt> for more examples with its scope
-
# equivalent.
-
#
-
# [:defaults]
-
# Sets defaults for parameters
-
#
-
# # Sets params[:format] to 'jpg' by default
-
# match 'path', to: 'c#a', defaults: { format: 'jpg' }, via: :get
-
#
-
# See <tt>Scoping#defaults</tt> for its scope equivalent.
-
#
-
# [:anchor]
-
# Boolean to anchor a <tt>match</tt> pattern. Default is true. When set to
-
# false, the pattern matches any request prefixed with the given path.
-
#
-
# # Matches any request starting with 'path'
-
# match 'path', to: 'c#a', anchor: false, via: :get
-
#
-
# [:format]
-
# Allows you to specify the default value for optional +format+
-
# segment or disable it by supplying +false+.
-
1
def match(path, options = nil)
-
end
-
-
# Mount a Rack-based application to be used within the application.
-
#
-
# mount SomeRackApp, at: "some_route"
-
#
-
# Alternatively:
-
#
-
# mount(SomeRackApp => "some_route")
-
#
-
# For options, see +match+, as +mount+ uses it internally.
-
#
-
# All mounted applications come with routing helpers to access them.
-
# These are named after the class specified, so for the above example
-
# the helper is either +some_rack_app_path+ or +some_rack_app_url+.
-
# To customize this helper's name, use the +:as+ option:
-
#
-
# mount(SomeRackApp => "some_route", as: "exciting")
-
#
-
# This will generate the +exciting_path+ and +exciting_url+ helpers
-
# which can be used to navigate to this mounted app.
-
1
def mount(app, options = nil)
-
2
if options
-
path = options.delete(:at)
-
1
elsif Hash === app
-
2
options = app
-
4
app, path = options.find { |k, _| k.respond_to?(:call) }
-
2
options.delete(app) if app
-
end
-
-
2
raise ArgumentError, "A rack application must be specified" unless app.respond_to?(:call)
-
2
raise ArgumentError, <<-MSG.strip_heredoc unless path
-
Must be called with mount point
-
-
mount SomeRackApp, at: "some_route"
-
or
-
mount(SomeRackApp => "some_route")
-
MSG
-
-
2
rails_app = rails_app? app
-
2
options[:as] ||= app_name(app, rails_app)
-
-
2
target_as = name_for_action(options[:as], path)
-
2
options[:via] ||= :all
-
-
2
match(path, options.merge(to: app, anchor: false, format: false))
-
-
2
define_generate_prefix(app, target_as) if rails_app
-
2
self
-
end
-
-
1
def default_url_options=(options)
-
@set.default_url_options = options
-
end
-
1
alias_method :default_url_options, :default_url_options=
-
-
1
def with_default_scope(scope, &block)
-
scope(scope) do
-
instance_exec(&block)
-
end
-
end
-
-
# Query if the following named route was already defined.
-
1
def has_named_route?(name)
-
11
@set.named_routes.key? name
-
end
-
-
1
private
-
1
def rails_app?(app)
-
2
app.is_a?(Class) && app < Rails::Railtie
-
end
-
-
1
def app_name(app, rails_app)
-
2
if rails_app
-
app.railtie_name
-
1
elsif app.is_a?(Class)
-
class_name = app.name
-
ActiveSupport::Inflector.underscore(class_name).tr("/", "_")
-
end
-
end
-
-
1
def define_generate_prefix(app, name)
-
_route = @set.named_routes.get name
-
_routes = @set
-
_url_helpers = @set.url_helpers
-
-
script_namer = ->(options) do
-
prefix_options = options.slice(*_route.segment_keys)
-
prefix_options[:relative_url_root] = "".freeze
-
-
if options[:_recall]
-
prefix_options.reverse_merge!(options[:_recall].slice(*_route.segment_keys))
-
end
-
-
# We must actually delete prefix segment keys to avoid passing them to next url_for.
-
_route.segment_keys.each { |k| options.delete(k) }
-
_url_helpers.send("#{name}_path", prefix_options)
-
end
-
-
app.routes.define_mounted_helper(name, script_namer)
-
-
app.routes.extend Module.new {
-
def optimize_routes_generation?; false; end
-
-
define_method :find_script_name do |options|
-
if options.key? :script_name
-
super(options)
-
else
-
script_namer.call(options)
-
end
-
end
-
}
-
end
-
end
-
-
1
module HttpHelpers
-
# Define a route that only recognizes HTTP GET.
-
# For supported arguments, see match[rdoc-ref:Base#match]
-
#
-
# get 'bacon', to: 'food#bacon'
-
1
def get(*args, &block)
-
12
map_method(:get, args, &block)
-
end
-
-
# Define a route that only recognizes HTTP POST.
-
# For supported arguments, see match[rdoc-ref:Base#match]
-
#
-
# post 'bacon', to: 'food#bacon'
-
1
def post(*args, &block)
-
2
map_method(:post, args, &block)
-
end
-
-
# Define a route that only recognizes HTTP PATCH.
-
# For supported arguments, see match[rdoc-ref:Base#match]
-
#
-
# patch 'bacon', to: 'food#bacon'
-
1
def patch(*args, &block)
-
map_method(:patch, args, &block)
-
end
-
-
# Define a route that only recognizes HTTP PUT.
-
# For supported arguments, see match[rdoc-ref:Base#match]
-
#
-
# put 'bacon', to: 'food#bacon'
-
1
def put(*args, &block)
-
1
map_method(:put, args, &block)
-
end
-
-
# Define a route that only recognizes HTTP DELETE.
-
# For supported arguments, see match[rdoc-ref:Base#match]
-
#
-
# delete 'broccoli', to: 'food#broccoli'
-
1
def delete(*args, &block)
-
map_method(:delete, args, &block)
-
end
-
-
1
private
-
1
def map_method(method, args, &block)
-
15
options = args.extract_options!
-
15
options[:via] = method
-
15
match(*args, options, &block)
-
15
self
-
end
-
end
-
-
# You may wish to organize groups of controllers under a namespace.
-
# Most commonly, you might group a number of administrative controllers
-
# under an +admin+ namespace. You would place these controllers under
-
# the <tt>app/controllers/admin</tt> directory, and you can group them
-
# together in your router:
-
#
-
# namespace "admin" do
-
# resources :posts, :comments
-
# end
-
#
-
# This will create a number of routes for each of the posts and comments
-
# controller. For <tt>Admin::PostsController</tt>, Rails will create:
-
#
-
# GET /admin/posts
-
# GET /admin/posts/new
-
# POST /admin/posts
-
# GET /admin/posts/1
-
# GET /admin/posts/1/edit
-
# PATCH/PUT /admin/posts/1
-
# DELETE /admin/posts/1
-
#
-
# If you want to route /posts (without the prefix /admin) to
-
# <tt>Admin::PostsController</tt>, you could use
-
#
-
# scope module: "admin" do
-
# resources :posts
-
# end
-
#
-
# or, for a single case
-
#
-
# resources :posts, module: "admin"
-
#
-
# If you want to route /admin/posts to +PostsController+
-
# (without the <tt>Admin::</tt> module prefix), you could use
-
#
-
# scope "/admin" do
-
# resources :posts
-
# end
-
#
-
# or, for a single case
-
#
-
# resources :posts, path: "/admin/posts"
-
#
-
# In each of these cases, the named routes remain the same as if you did
-
# not use scope. In the last case, the following paths map to
-
# +PostsController+:
-
#
-
# GET /admin/posts
-
# GET /admin/posts/new
-
# POST /admin/posts
-
# GET /admin/posts/1
-
# GET /admin/posts/1/edit
-
# PATCH/PUT /admin/posts/1
-
# DELETE /admin/posts/1
-
1
module Scoping
-
# Scopes a set of routes to the given default options.
-
#
-
# Take the following route definition as an example:
-
#
-
# scope path: ":account_id", as: "account" do
-
# resources :projects
-
# end
-
#
-
# This generates helpers such as +account_projects_path+, just like +resources+ does.
-
# The difference here being that the routes generated are like /:account_id/projects,
-
# rather than /accounts/:account_id/projects.
-
#
-
# === Options
-
#
-
# Takes same options as <tt>Base#match</tt> and <tt>Resources#resources</tt>.
-
#
-
# # route /posts (without the prefix /admin) to <tt>Admin::PostsController</tt>
-
# scope module: "admin" do
-
# resources :posts
-
# end
-
#
-
# # prefix the posts resource's requests with '/admin'
-
# scope path: "/admin" do
-
# resources :posts
-
# end
-
#
-
# # prefix the routing helper name: +sekret_posts_path+ instead of +posts_path+
-
# scope as: "sekret" do
-
# resources :posts
-
# end
-
1
def scope(*args)
-
options = args.extract_options!.dup
-
scope = {}
-
-
options[:path] = args.flatten.join("/") if args.any?
-
options[:constraints] ||= {}
-
-
unless nested_scope?
-
options[:shallow_path] ||= options[:path] if options.key?(:path)
-
options[:shallow_prefix] ||= options[:as] if options.key?(:as)
-
end
-
-
if options[:constraints].is_a?(Hash)
-
defaults = options[:constraints].select do |k, v|
-
URL_OPTIONS.include?(k) && (v.is_a?(String) || v.is_a?(Integer))
-
end
-
-
options[:defaults] = defaults.merge(options[:defaults] || {})
-
else
-
block, options[:constraints] = options[:constraints], {}
-
end
-
-
if options.key?(:only) || options.key?(:except)
-
scope[:action_options] = { only: options.delete(:only),
-
except: options.delete(:except) }
-
end
-
-
if options.key? :anchor
-
raise ArgumentError, "anchor is ignored unless passed to `match`"
-
end
-
-
@scope.options.each do |option|
-
if option == :blocks
-
value = block
-
elsif option == :options
-
value = options
-
else
-
value = options.delete(option) { POISON }
-
end
-
-
unless POISON == value
-
scope[option] = send("merge_#{option}_scope", @scope[option], value)
-
end
-
end
-
-
@scope = @scope.new scope
-
yield
-
self
-
ensure
-
@scope = @scope.parent
-
end
-
-
1
POISON = Object.new # :nodoc:
-
-
# Scopes routes to a specific controller
-
#
-
# controller "food" do
-
# match "bacon", action: :bacon, via: :get
-
# end
-
1
def controller(controller)
-
@scope = @scope.new(controller: controller)
-
yield
-
ensure
-
@scope = @scope.parent
-
end
-
-
# Scopes routes to a specific namespace. For example:
-
#
-
# namespace :admin do
-
# resources :posts
-
# end
-
#
-
# This generates the following routes:
-
#
-
# admin_posts GET /admin/posts(.:format) admin/posts#index
-
# admin_posts POST /admin/posts(.:format) admin/posts#create
-
# new_admin_post GET /admin/posts/new(.:format) admin/posts#new
-
# edit_admin_post GET /admin/posts/:id/edit(.:format) admin/posts#edit
-
# admin_post GET /admin/posts/:id(.:format) admin/posts#show
-
# admin_post PATCH/PUT /admin/posts/:id(.:format) admin/posts#update
-
# admin_post DELETE /admin/posts/:id(.:format) admin/posts#destroy
-
#
-
# === Options
-
#
-
# The +:path+, +:as+, +:module+, +:shallow_path+ and +:shallow_prefix+
-
# options all default to the name of the namespace.
-
#
-
# For options, see <tt>Base#match</tt>. For +:shallow_path+ option, see
-
# <tt>Resources#resources</tt>.
-
#
-
# # accessible through /sekret/posts rather than /admin/posts
-
# namespace :admin, path: "sekret" do
-
# resources :posts
-
# end
-
#
-
# # maps to <tt>Sekret::PostsController</tt> rather than <tt>Admin::PostsController</tt>
-
# namespace :admin, module: "sekret" do
-
# resources :posts
-
# end
-
#
-
# # generates +sekret_posts_path+ rather than +admin_posts_path+
-
# namespace :admin, as: "sekret" do
-
# resources :posts
-
# end
-
1
def namespace(path, options = {})
-
path = path.to_s
-
-
defaults = {
-
module: path,
-
as: options.fetch(:as, path),
-
shallow_path: options.fetch(:path, path),
-
shallow_prefix: options.fetch(:as, path)
-
}
-
-
path_scope(options.delete(:path) { path }) do
-
scope(defaults.merge!(options)) { yield }
-
end
-
end
-
-
# === Parameter Restriction
-
# Allows you to constrain the nested routes based on a set of rules.
-
# For instance, in order to change the routes to allow for a dot character in the +id+ parameter:
-
#
-
# constraints(id: /\d+\.\d+/) do
-
# resources :posts
-
# end
-
#
-
# Now routes such as +/posts/1+ will no longer be valid, but +/posts/1.1+ will be.
-
# The +id+ parameter must match the constraint passed in for this example.
-
#
-
# You may use this to also restrict other parameters:
-
#
-
# resources :posts do
-
# constraints(post_id: /\d+\.\d+/) do
-
# resources :comments
-
# end
-
# end
-
#
-
# === Restricting based on IP
-
#
-
# Routes can also be constrained to an IP or a certain range of IP addresses:
-
#
-
# constraints(ip: /192\.168\.\d+\.\d+/) do
-
# resources :posts
-
# end
-
#
-
# Any user connecting from the 192.168.* range will be able to see this resource,
-
# where as any user connecting outside of this range will be told there is no such route.
-
#
-
# === Dynamic request matching
-
#
-
# Requests to routes can be constrained based on specific criteria:
-
#
-
# constraints(-> (req) { req.env["HTTP_USER_AGENT"] =~ /iPhone/ }) do
-
# resources :iphones
-
# end
-
#
-
# You are able to move this logic out into a class if it is too complex for routes.
-
# This class must have a +matches?+ method defined on it which either returns +true+
-
# if the user should be given access to that route, or +false+ if the user should not.
-
#
-
# class Iphone
-
# def self.matches?(request)
-
# request.env["HTTP_USER_AGENT"] =~ /iPhone/
-
# end
-
# end
-
#
-
# An expected place for this code would be +lib/constraints+.
-
#
-
# This class is then used like this:
-
#
-
# constraints(Iphone) do
-
# resources :iphones
-
# end
-
1
def constraints(constraints = {})
-
scope(constraints: constraints) { yield }
-
end
-
-
# Allows you to set default parameters for a route, such as this:
-
# defaults id: 'home' do
-
# match 'scoped_pages/(:id)', to: 'pages#show'
-
# end
-
# Using this, the +:id+ parameter here will default to 'home'.
-
1
def defaults(defaults = {})
-
@scope = @scope.new(defaults: merge_defaults_scope(@scope[:defaults], defaults))
-
yield
-
ensure
-
@scope = @scope.parent
-
end
-
-
1
private
-
1
def merge_path_scope(parent, child)
-
Mapper.normalize_path("#{parent}/#{child}")
-
end
-
-
1
def merge_shallow_path_scope(parent, child)
-
Mapper.normalize_path("#{parent}/#{child}")
-
end
-
-
1
def merge_as_scope(parent, child)
-
parent ? "#{parent}_#{child}" : child
-
end
-
-
1
def merge_shallow_prefix_scope(parent, child)
-
parent ? "#{parent}_#{child}" : child
-
end
-
-
1
def merge_module_scope(parent, child)
-
parent ? "#{parent}/#{child}" : child
-
end
-
-
1
def merge_controller_scope(parent, child)
-
child
-
end
-
-
1
def merge_action_scope(parent, child)
-
child
-
end
-
-
1
def merge_via_scope(parent, child)
-
child
-
end
-
-
1
def merge_format_scope(parent, child)
-
child
-
end
-
-
1
def merge_path_names_scope(parent, child)
-
merge_options_scope(parent, child)
-
end
-
-
1
def merge_constraints_scope(parent, child)
-
merge_options_scope(parent, child)
-
end
-
-
1
def merge_defaults_scope(parent, child)
-
merge_options_scope(parent, child)
-
end
-
-
1
def merge_blocks_scope(parent, child)
-
merged = parent ? parent.dup : []
-
merged << child if child
-
merged
-
end
-
-
1
def merge_options_scope(parent, child)
-
(parent || {}).merge(child)
-
end
-
-
1
def merge_shallow_scope(parent, child)
-
child ? true : false
-
end
-
-
1
def merge_to_scope(parent, child)
-
child
-
end
-
end
-
-
# Resource routing allows you to quickly declare all of the common routes
-
# for a given resourceful controller. Instead of declaring separate routes
-
# for your +index+, +show+, +new+, +edit+, +create+, +update+ and +destroy+
-
# actions, a resourceful route declares them in a single line of code:
-
#
-
# resources :photos
-
#
-
# Sometimes, you have a resource that clients always look up without
-
# referencing an ID. A common example, /profile always shows the profile of
-
# the currently logged in user. In this case, you can use a singular resource
-
# to map /profile (rather than /profile/:id) to the show action.
-
#
-
# resource :profile
-
#
-
# It's common to have resources that are logically children of other
-
# resources:
-
#
-
# resources :magazines do
-
# resources :ads
-
# end
-
#
-
# You may wish to organize groups of controllers under a namespace. Most
-
# commonly, you might group a number of administrative controllers under
-
# an +admin+ namespace. You would place these controllers under the
-
# <tt>app/controllers/admin</tt> directory, and you can group them together
-
# in your router:
-
#
-
# namespace "admin" do
-
# resources :posts, :comments
-
# end
-
#
-
# By default the +:id+ parameter doesn't accept dots. If you need to
-
# use dots as part of the +:id+ parameter add a constraint which
-
# overrides this restriction, e.g:
-
#
-
# resources :articles, id: /[^\/]+/
-
#
-
# This allows any character other than a slash as part of your +:id+.
-
#
-
1
module Resources
-
# CANONICAL_ACTIONS holds all actions that does not need a prefix or
-
# a path appended since they fit properly in their scope level.
-
1
VALID_ON_OPTIONS = [:new, :collection, :member]
-
1
RESOURCE_OPTIONS = [:as, :controller, :path, :only, :except, :param, :concerns]
-
1
CANONICAL_ACTIONS = %w(index create new show update destroy)
-
-
1
class Resource #:nodoc:
-
1
attr_reader :controller, :path, :param
-
-
1
def initialize(entities, api_only, shallow, options = {})
-
@name = entities.to_s
-
@path = (options[:path] || @name).to_s
-
@controller = (options[:controller] || @name).to_s
-
@as = options[:as]
-
@param = (options[:param] || :id).to_sym
-
@options = options
-
@shallow = shallow
-
@api_only = api_only
-
@only = options.delete :only
-
@except = options.delete :except
-
end
-
-
1
def default_actions
-
if @api_only
-
[:index, :create, :show, :update, :destroy]
-
else
-
[:index, :create, :new, :show, :update, :destroy, :edit]
-
end
-
end
-
-
1
def actions
-
if @only
-
Array(@only).map(&:to_sym)
-
elsif @except
-
default_actions - Array(@except).map(&:to_sym)
-
else
-
default_actions
-
end
-
end
-
-
1
def name
-
@as || @name
-
end
-
-
1
def plural
-
@plural ||= name.to_s
-
end
-
-
1
def singular
-
@singular ||= name.to_s.singularize
-
end
-
-
1
alias :member_name :singular
-
-
# Checks for uncountable plurals, and appends "_index" if the plural
-
# and singular form are the same.
-
1
def collection_name
-
singular == plural ? "#{plural}_index" : plural
-
end
-
-
1
def resource_scope
-
controller
-
end
-
-
1
alias :collection_scope :path
-
-
1
def member_scope
-
"#{path}/:#{param}"
-
end
-
-
1
alias :shallow_scope :member_scope
-
-
1
def new_scope(new_path)
-
"#{path}/#{new_path}"
-
end
-
-
1
def nested_param
-
:"#{singular}_#{param}"
-
end
-
-
1
def nested_scope
-
"#{path}/:#{nested_param}"
-
end
-
-
1
def shallow?
-
@shallow
-
end
-
-
1
def singleton?; false; end
-
end
-
-
1
class SingletonResource < Resource #:nodoc:
-
1
def initialize(entities, api_only, shallow, options)
-
super
-
@as = nil
-
@controller = (options[:controller] || plural).to_s
-
@as = options[:as]
-
end
-
-
1
def default_actions
-
if @api_only
-
[:show, :create, :update, :destroy]
-
else
-
[:show, :create, :update, :destroy, :new, :edit]
-
end
-
end
-
-
1
def plural
-
@plural ||= name.to_s.pluralize
-
end
-
-
1
def singular
-
@singular ||= name.to_s
-
end
-
-
1
alias :member_name :singular
-
1
alias :collection_name :singular
-
-
1
alias :member_scope :path
-
1
alias :nested_scope :path
-
-
1
def singleton?; true; end
-
end
-
-
1
def resources_path_names(options)
-
@scope[:path_names].merge!(options)
-
end
-
-
# Sometimes, you have a resource that clients always look up without
-
# referencing an ID. A common example, /profile always shows the
-
# profile of the currently logged in user. In this case, you can use
-
# a singular resource to map /profile (rather than /profile/:id) to
-
# the show action:
-
#
-
# resource :profile
-
#
-
# This creates six different routes in your application, all mapping to
-
# the +Profiles+ controller (note that the controller is named after
-
# the plural):
-
#
-
# GET /profile/new
-
# GET /profile
-
# GET /profile/edit
-
# PATCH/PUT /profile
-
# DELETE /profile
-
# POST /profile
-
#
-
# === Options
-
# Takes same options as resources[rdoc-ref:#resources]
-
1
def resource(*resources, &block)
-
options = resources.extract_options!.dup
-
-
if apply_common_behavior_for(:resource, resources, options, &block)
-
return self
-
end
-
-
with_scope_level(:resource) do
-
options = apply_action_options options
-
resource_scope(SingletonResource.new(resources.pop, api_only?, @scope[:shallow], options)) do
-
yield if block_given?
-
-
concerns(options[:concerns]) if options[:concerns]
-
-
new do
-
get :new
-
end if parent_resource.actions.include?(:new)
-
-
set_member_mappings_for_resource
-
-
collection do
-
post :create
-
end if parent_resource.actions.include?(:create)
-
end
-
end
-
-
self
-
end
-
-
# In Rails, a resourceful route provides a mapping between HTTP verbs
-
# and URLs and controller actions. By convention, each action also maps
-
# to particular CRUD operations in a database. A single entry in the
-
# routing file, such as
-
#
-
# resources :photos
-
#
-
# creates seven different routes in your application, all mapping to
-
# the +Photos+ controller:
-
#
-
# GET /photos
-
# GET /photos/new
-
# POST /photos
-
# GET /photos/:id
-
# GET /photos/:id/edit
-
# PATCH/PUT /photos/:id
-
# DELETE /photos/:id
-
#
-
# Resources can also be nested infinitely by using this block syntax:
-
#
-
# resources :photos do
-
# resources :comments
-
# end
-
#
-
# This generates the following comments routes:
-
#
-
# GET /photos/:photo_id/comments
-
# GET /photos/:photo_id/comments/new
-
# POST /photos/:photo_id/comments
-
# GET /photos/:photo_id/comments/:id
-
# GET /photos/:photo_id/comments/:id/edit
-
# PATCH/PUT /photos/:photo_id/comments/:id
-
# DELETE /photos/:photo_id/comments/:id
-
#
-
# === Options
-
# Takes same options as match[rdoc-ref:Base#match] as well as:
-
#
-
# [:path_names]
-
# Allows you to change the segment component of the +edit+ and +new+ actions.
-
# Actions not specified are not changed.
-
#
-
# resources :posts, path_names: { new: "brand_new" }
-
#
-
# The above example will now change /posts/new to /posts/brand_new.
-
#
-
# [:path]
-
# Allows you to change the path prefix for the resource.
-
#
-
# resources :posts, path: 'postings'
-
#
-
# The resource and all segments will now route to /postings instead of /posts.
-
#
-
# [:only]
-
# Only generate routes for the given actions.
-
#
-
# resources :cows, only: :show
-
# resources :cows, only: [:show, :index]
-
#
-
# [:except]
-
# Generate all routes except for the given actions.
-
#
-
# resources :cows, except: :show
-
# resources :cows, except: [:show, :index]
-
#
-
# [:shallow]
-
# Generates shallow routes for nested resource(s). When placed on a parent resource,
-
# generates shallow routes for all nested resources.
-
#
-
# resources :posts, shallow: true do
-
# resources :comments
-
# end
-
#
-
# Is the same as:
-
#
-
# resources :posts do
-
# resources :comments, except: [:show, :edit, :update, :destroy]
-
# end
-
# resources :comments, only: [:show, :edit, :update, :destroy]
-
#
-
# This allows URLs for resources that otherwise would be deeply nested such
-
# as a comment on a blog post like <tt>/posts/a-long-permalink/comments/1234</tt>
-
# to be shortened to just <tt>/comments/1234</tt>.
-
#
-
# [:shallow_path]
-
# Prefixes nested shallow routes with the specified path.
-
#
-
# scope shallow_path: "sekret" do
-
# resources :posts do
-
# resources :comments, shallow: true
-
# end
-
# end
-
#
-
# The +comments+ resource here will have the following routes generated for it:
-
#
-
# post_comments GET /posts/:post_id/comments(.:format)
-
# post_comments POST /posts/:post_id/comments(.:format)
-
# new_post_comment GET /posts/:post_id/comments/new(.:format)
-
# edit_comment GET /sekret/comments/:id/edit(.:format)
-
# comment GET /sekret/comments/:id(.:format)
-
# comment PATCH/PUT /sekret/comments/:id(.:format)
-
# comment DELETE /sekret/comments/:id(.:format)
-
#
-
# [:shallow_prefix]
-
# Prefixes nested shallow route names with specified prefix.
-
#
-
# scope shallow_prefix: "sekret" do
-
# resources :posts do
-
# resources :comments, shallow: true
-
# end
-
# end
-
#
-
# The +comments+ resource here will have the following routes generated for it:
-
#
-
# post_comments GET /posts/:post_id/comments(.:format)
-
# post_comments POST /posts/:post_id/comments(.:format)
-
# new_post_comment GET /posts/:post_id/comments/new(.:format)
-
# edit_sekret_comment GET /comments/:id/edit(.:format)
-
# sekret_comment GET /comments/:id(.:format)
-
# sekret_comment PATCH/PUT /comments/:id(.:format)
-
# sekret_comment DELETE /comments/:id(.:format)
-
#
-
# [:format]
-
# Allows you to specify the default value for optional +format+
-
# segment or disable it by supplying +false+.
-
#
-
# === Examples
-
#
-
# # routes call <tt>Admin::PostsController</tt>
-
# resources :posts, module: "admin"
-
#
-
# # resource actions are at /admin/posts.
-
# resources :posts, path: "admin/posts"
-
1
def resources(*resources, &block)
-
options = resources.extract_options!.dup
-
-
if apply_common_behavior_for(:resources, resources, options, &block)
-
return self
-
end
-
-
with_scope_level(:resources) do
-
options = apply_action_options options
-
resource_scope(Resource.new(resources.pop, api_only?, @scope[:shallow], options)) do
-
yield if block_given?
-
-
concerns(options[:concerns]) if options[:concerns]
-
-
collection do
-
get :index if parent_resource.actions.include?(:index)
-
post :create if parent_resource.actions.include?(:create)
-
end
-
-
new do
-
get :new
-
end if parent_resource.actions.include?(:new)
-
-
set_member_mappings_for_resource
-
end
-
end
-
-
self
-
end
-
-
# To add a route to the collection:
-
#
-
# resources :photos do
-
# collection do
-
# get 'search'
-
# end
-
# end
-
#
-
# This will enable Rails to recognize paths such as <tt>/photos/search</tt>
-
# with GET, and route to the search action of +PhotosController+. It will also
-
# create the <tt>search_photos_url</tt> and <tt>search_photos_path</tt>
-
# route helpers.
-
1
def collection
-
unless resource_scope?
-
raise ArgumentError, "can't use collection outside resource(s) scope"
-
end
-
-
with_scope_level(:collection) do
-
path_scope(parent_resource.collection_scope) do
-
yield
-
end
-
end
-
end
-
-
# To add a member route, add a member block into the resource block:
-
#
-
# resources :photos do
-
# member do
-
# get 'preview'
-
# end
-
# end
-
#
-
# This will recognize <tt>/photos/1/preview</tt> with GET, and route to the
-
# preview action of +PhotosController+. It will also create the
-
# <tt>preview_photo_url</tt> and <tt>preview_photo_path</tt> helpers.
-
1
def member
-
unless resource_scope?
-
raise ArgumentError, "can't use member outside resource(s) scope"
-
end
-
-
with_scope_level(:member) do
-
if shallow?
-
shallow_scope {
-
path_scope(parent_resource.member_scope) { yield }
-
}
-
else
-
path_scope(parent_resource.member_scope) { yield }
-
end
-
end
-
end
-
-
1
def new
-
unless resource_scope?
-
raise ArgumentError, "can't use new outside resource(s) scope"
-
end
-
-
with_scope_level(:new) do
-
path_scope(parent_resource.new_scope(action_path(:new))) do
-
yield
-
end
-
end
-
end
-
-
1
def nested
-
unless resource_scope?
-
raise ArgumentError, "can't use nested outside resource(s) scope"
-
end
-
-
with_scope_level(:nested) do
-
if shallow? && shallow_nesting_depth >= 1
-
shallow_scope do
-
path_scope(parent_resource.nested_scope) do
-
scope(nested_options) { yield }
-
end
-
end
-
else
-
path_scope(parent_resource.nested_scope) do
-
scope(nested_options) { yield }
-
end
-
end
-
end
-
end
-
-
# See ActionDispatch::Routing::Mapper::Scoping#namespace.
-
1
def namespace(path, options = {})
-
if resource_scope?
-
nested { super }
-
else
-
super
-
end
-
end
-
-
1
def shallow
-
@scope = @scope.new(shallow: true)
-
yield
-
ensure
-
@scope = @scope.parent
-
end
-
-
1
def shallow?
-
!parent_resource.singleton? && @scope[:shallow]
-
end
-
-
# Matches a URL pattern to one or more routes.
-
# For more information, see match[rdoc-ref:Base#match].
-
#
-
# match 'path' => 'controller#action', via: :patch
-
# match 'path', to: 'controller#action', via: :post
-
# match 'path', 'otherpath', on: :member, via: :get
-
1
def match(path, *rest, &block)
-
18
if rest.empty? && Hash === path
-
15
options = path
-
30
path, to = options.find { |name, _value| name.is_a?(String) }
-
-
15
raise ArgumentError, "Route path not specified" if path.nil?
-
-
15
case to
-
when Symbol
-
options[:action] = to
-
when String
-
15
if to =~ /#/
-
15
options[:to] = to
-
else
-
options[:controller] = to
-
end
-
else
-
options[:to] = to
-
end
-
-
15
options.delete(path)
-
15
paths = [path]
-
else
-
3
options = rest.pop || {}
-
3
paths = [path] + rest
-
end
-
-
18
if options.key?(:defaults)
-
defaults(options.delete(:defaults)) { map_match(paths, options, &block) }
-
else
-
18
map_match(paths, options, &block)
-
end
-
end
-
-
# You can specify what Rails should route "/" to with the root method:
-
#
-
# root to: 'pages#main'
-
#
-
# For options, see +match+, as +root+ uses it internally.
-
#
-
# You can also pass a string which will expand
-
#
-
# root 'pages#main'
-
#
-
# You should put the root route at the top of <tt>config/routes.rb</tt>,
-
# because this means it will be matched first. As this is the most popular route
-
# of most Rails applications, this is beneficial.
-
1
def root(path, options = {})
-
1
if path.is_a?(String)
-
options[:to] = path
-
elsif path.is_a?(Hash) && options.empty?
-
1
options = path
-
else
-
raise ArgumentError, "must be called with a path and/or options"
-
end
-
-
1
if @scope.resources?
-
with_scope_level(:root) do
-
path_scope(parent_resource.path) do
-
match_root_route(options)
-
end
-
end
-
else
-
1
match_root_route(options)
-
end
-
end
-
-
1
private
-
-
1
def parent_resource
-
19
@scope[:scope_level_resource]
-
end
-
-
1
def apply_common_behavior_for(method, resources, options, &block)
-
if resources.length > 1
-
resources.each { |r| send(method, r, options, &block) }
-
return true
-
end
-
-
if options.delete(:shallow)
-
shallow do
-
send(method, resources.pop, options, &block)
-
end
-
return true
-
end
-
-
if resource_scope?
-
nested { send(method, resources.pop, options, &block) }
-
return true
-
end
-
-
options.keys.each do |k|
-
(options[:constraints] ||= {})[k] = options.delete(k) if options[k].is_a?(Regexp)
-
end
-
-
scope_options = options.slice!(*RESOURCE_OPTIONS)
-
unless scope_options.empty?
-
scope(scope_options) do
-
send(method, resources.pop, options, &block)
-
end
-
return true
-
end
-
-
false
-
end
-
-
1
def apply_action_options(options)
-
return options if action_options? options
-
options.merge scope_action_options
-
end
-
-
1
def action_options?(options)
-
options[:only] || options[:except]
-
end
-
-
1
def scope_action_options
-
@scope[:action_options] || {}
-
end
-
-
1
def resource_scope?
-
@scope.resource_scope?
-
end
-
-
1
def resource_method_scope?
-
12
@scope.resource_method_scope?
-
end
-
-
1
def nested_scope?
-
@scope.nested?
-
end
-
-
1
def with_scope_level(kind) # :doc:
-
@scope = @scope.new_level(kind)
-
yield
-
ensure
-
@scope = @scope.parent
-
end
-
-
1
def resource_scope(resource)
-
@scope = @scope.new(scope_level_resource: resource)
-
-
controller(resource.resource_scope) { yield }
-
ensure
-
@scope = @scope.parent
-
end
-
-
1
def nested_options
-
options = { as: parent_resource.member_name }
-
options[:constraints] = {
-
parent_resource.nested_param => param_constraint
-
} if param_constraint?
-
-
options
-
end
-
-
1
def shallow_nesting_depth
-
@scope.find_all { |node|
-
node.frame[:scope_level_resource]
-
}.count { |node| node.frame[:scope_level_resource].shallow? }
-
end
-
-
1
def param_constraint?
-
@scope[:constraints] && @scope[:constraints][parent_resource.param].is_a?(Regexp)
-
end
-
-
1
def param_constraint
-
@scope[:constraints][parent_resource.param]
-
end
-
-
1
def canonical_action?(action)
-
12
resource_method_scope? && CANONICAL_ACTIONS.include?(action.to_s)
-
end
-
-
1
def shallow_scope
-
scope = { as: @scope[:shallow_prefix],
-
path: @scope[:shallow_path] }
-
@scope = @scope.new scope
-
-
yield
-
ensure
-
@scope = @scope.parent
-
end
-
-
1
def path_for_action(action, path)
-
18
return "#{@scope[:path]}/#{path}" if path
-
-
if canonical_action?(action)
-
@scope[:path].to_s
-
else
-
"#{@scope[:path]}/#{action_path(action)}"
-
end
-
end
-
-
1
def action_path(name)
-
@scope[:path_names][name.to_sym] || name
-
end
-
-
1
def prefix_name_for_action(as, action)
-
19
if as
-
7
prefix = as
-
11
elsif !canonical_action?(action)
-
12
prefix = action
-
end
-
-
19
if prefix && prefix != "/" && !prefix.empty?
-
17
Mapper.normalize_name prefix.to_s.tr("-", "_")
-
end
-
end
-
-
1
def name_for_action(as, action)
-
19
prefix = prefix_name_for_action(as, action)
-
19
name_prefix = @scope[:as]
-
-
19
if parent_resource
-
return nil unless as || action
-
-
collection_name = parent_resource.collection_name
-
member_name = parent_resource.member_name
-
end
-
-
19
action_name = @scope.action_name(name_prefix, prefix, collection_name, member_name)
-
19
candidate = action_name.select(&:present?).join("_")
-
-
19
unless candidate.empty?
-
# If a name was not explicitly given, we check if it is valid
-
# and return nil in case it isn't. Otherwise, we pass the invalid name
-
# forward so the underlying router engine treats it and raises an exception.
-
17
if as.nil?
-
10
candidate unless candidate !~ /\A[_a-z]/i || has_named_route?(candidate)
-
else
-
7
candidate
-
end
-
end
-
end
-
-
1
def set_member_mappings_for_resource # :doc:
-
member do
-
get :edit if parent_resource.actions.include?(:edit)
-
get :show if parent_resource.actions.include?(:show)
-
if parent_resource.actions.include?(:update)
-
patch :update
-
put :update
-
end
-
delete :destroy if parent_resource.actions.include?(:destroy)
-
end
-
end
-
-
1
def api_only? # :doc:
-
@set.api_only?
-
end
-
-
1
def path_scope(path)
-
@scope = @scope.new(path: merge_path_scope(@scope[:path], path))
-
yield
-
ensure
-
@scope = @scope.parent
-
end
-
-
1
def map_match(paths, options)
-
18
if options[:on] && !VALID_ON_OPTIONS.include?(options[:on])
-
raise ArgumentError, "Unknown scope #{on.inspect} given to :on"
-
end
-
-
18
if @scope[:to]
-
options[:to] ||= @scope[:to]
-
end
-
-
18
if @scope[:controller] && @scope[:action]
-
options[:to] ||= "#{@scope[:controller]}##{@scope[:action]}"
-
end
-
-
18
controller = options.delete(:controller) || @scope[:controller]
-
18
option_path = options.delete :path
-
18
to = options.delete :to
-
18
via = Mapping.check_via Array(options.delete(:via) {
-
@scope[:via]
-
})
-
34
formatted = options.delete(:format) { @scope[:format] }
-
34
anchor = options.delete(:anchor) { true }
-
18
options_constraints = options.delete(:constraints) || {}
-
-
18
path_types = paths.group_by(&:class)
-
18
path_types.fetch(String, []).each do |_path|
-
18
route_options = options.dup
-
18
if _path && option_path
-
raise ArgumentError, "Ambiguous route definition. Both :path and the route path were specified as strings."
-
end
-
18
to = get_to_from_path(_path, to, route_options[:action])
-
18
decomposed_match(_path, controller, route_options, _path, to, via, formatted, anchor, options_constraints)
-
end
-
-
18
path_types.fetch(Symbol, []).each do |action|
-
route_options = options.dup
-
decomposed_match(action, controller, route_options, option_path, to, via, formatted, anchor, options_constraints)
-
end
-
-
18
self
-
end
-
-
1
def get_to_from_path(path, to, action)
-
18
return to if to || action
-
-
path_without_format = path.sub(/\(\.:format\)$/, "")
-
if using_match_shorthand?(path_without_format)
-
path_without_format.gsub(%r{^/}, "").sub(%r{/([^/]*)$}, '#\1').tr("-", "_")
-
else
-
nil
-
end
-
end
-
-
1
def using_match_shorthand?(path)
-
path =~ %r{^/?[-\w]+/[-\w/]+$}
-
end
-
-
1
def decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints)
-
18
if on = options.delete(:on)
-
send(on) { decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints) }
-
else
-
18
case @scope.scope_level
-
when :resources
-
nested { decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints) }
-
when :resource
-
member { decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints) }
-
else
-
18
add_route(path, controller, options, _path, to, via, formatted, anchor, options_constraints)
-
end
-
end
-
end
-
-
1
def add_route(action, controller, options, _path, to, via, formatted, anchor, options_constraints)
-
18
path = path_for_action(action, _path)
-
18
raise ArgumentError, "path is required" if path.blank?
-
-
18
action = action.to_s
-
-
18
default_action = options.delete(:action) || @scope[:action]
-
-
18
if action =~ /^[\w\-\/]+$/
-
13
default_action ||= action.tr("-", "_") unless action.include?("/")
-
else
-
5
action = nil
-
end
-
-
18
as = if !options.fetch(:as, true) # if it's set to nil or false
-
2
options.delete(:as)
-
else
-
16
name_for_action(options.delete(:as), action)
-
end
-
-
18
path = Mapping.normalize_path URI.parser.escape(path), formatted
-
18
ast = Journey::Parser.parse path
-
-
18
mapping = Mapping.build(@scope, @set, ast, controller, default_action, to, via, formatted, options_constraints, anchor, options)
-
18
@set.add_route(mapping, as)
-
end
-
-
1
def match_root_route(options)
-
1
name = has_named_route?(name_for_action(:root, nil)) ? nil : :root
-
1
args = ["/", { as: name, via: :get }.merge!(options)]
-
-
1
match(*args)
-
end
-
end
-
-
# Routing Concerns allow you to declare common routes that can be reused
-
# inside others resources and routes.
-
#
-
# concern :commentable do
-
# resources :comments
-
# end
-
#
-
# concern :image_attachable do
-
# resources :images, only: :index
-
# end
-
#
-
# These concerns are used in Resources routing:
-
#
-
# resources :messages, concerns: [:commentable, :image_attachable]
-
#
-
# or in a scope or namespace:
-
#
-
# namespace :posts do
-
# concerns :commentable
-
# end
-
1
module Concerns
-
# Define a routing concern using a name.
-
#
-
# Concerns may be defined inline, using a block, or handled by
-
# another object, by passing that object as the second parameter.
-
#
-
# The concern object, if supplied, should respond to <tt>call</tt>,
-
# which will receive two parameters:
-
#
-
# * The current mapper
-
# * A hash of options which the concern object may use
-
#
-
# Options may also be used by concerns defined in a block by accepting
-
# a block parameter. So, using a block, you might do something as
-
# simple as limit the actions available on certain resources, passing
-
# standard resource options through the concern:
-
#
-
# concern :commentable do |options|
-
# resources :comments, options
-
# end
-
#
-
# resources :posts, concerns: :commentable
-
# resources :archived_posts do
-
# # Don't allow comments on archived posts
-
# concerns :commentable, only: [:index, :show]
-
# end
-
#
-
# Or, using a callable object, you might implement something more
-
# specific to your application, which would be out of place in your
-
# routes file.
-
#
-
# # purchasable.rb
-
# class Purchasable
-
# def initialize(defaults = {})
-
# @defaults = defaults
-
# end
-
#
-
# def call(mapper, options = {})
-
# options = @defaults.merge(options)
-
# mapper.resources :purchases
-
# mapper.resources :receipts
-
# mapper.resources :returns if options[:returnable]
-
# end
-
# end
-
#
-
# # routes.rb
-
# concern :purchasable, Purchasable.new(returnable: true)
-
#
-
# resources :toys, concerns: :purchasable
-
# resources :electronics, concerns: :purchasable
-
# resources :pets do
-
# concerns :purchasable, returnable: false
-
# end
-
#
-
# Any routing helpers can be used inside a concern. If using a
-
# callable, they're accessible from the Mapper that's passed to
-
# <tt>call</tt>.
-
1
def concern(name, callable = nil, &block)
-
callable ||= lambda { |mapper, options| mapper.instance_exec(options, &block) }
-
@concerns[name] = callable
-
end
-
-
# Use the named concerns
-
#
-
# resources :posts do
-
# concerns :commentable
-
# end
-
#
-
# Concerns also work in any routes helper that you want to use:
-
#
-
# namespace :posts do
-
# concerns :commentable
-
# end
-
1
def concerns(*args)
-
options = args.extract_options!
-
args.flatten.each do |name|
-
if concern = @concerns[name]
-
concern.call(self, options)
-
else
-
raise ArgumentError, "No concern named #{name} was found!"
-
end
-
end
-
end
-
end
-
-
1
module CustomUrls
-
# Define custom URL helpers that will be added to the application's
-
# routes. This allows you to override and/or replace the default behavior
-
# of routing helpers, e.g:
-
#
-
# direct :homepage do
-
# "http://www.rubyonrails.org"
-
# end
-
#
-
# direct :commentable do |model|
-
# [ model, anchor: model.dom_id ]
-
# end
-
#
-
# direct :main do
-
# { controller: "pages", action: "index", subdomain: "www" }
-
# end
-
#
-
# The return value from the block passed to +direct+ must be a valid set of
-
# arguments for +url_for+ which will actually build the URL string. This can
-
# be one of the following:
-
#
-
# * A string, which is treated as a generated URL
-
# * A hash, e.g. <tt>{ controller: "pages", action: "index" }</tt>
-
# * An array, which is passed to +polymorphic_url+
-
# * An Active Model instance
-
# * An Active Model class
-
#
-
# NOTE: Other URL helpers can be called in the block but be careful not to invoke
-
# your custom URL helper again otherwise it will result in a stack overflow error.
-
#
-
# You can also specify default options that will be passed through to
-
# your URL helper definition, e.g:
-
#
-
# direct :browse, page: 1, size: 10 do |options|
-
# [ :products, options.merge(params.permit(:page, :size).to_h.symbolize_keys) ]
-
# end
-
#
-
# In this instance the +params+ object comes from the context in which the
-
# block is executed, e.g. generating a URL inside a controller action or a view.
-
# If the block is executed where there isn't a +params+ object such as this:
-
#
-
# Rails.application.routes.url_helpers.browse_path
-
#
-
# then it will raise a +NameError+. Because of this you need to be aware of the
-
# context in which you will use your custom URL helper when defining it.
-
#
-
# NOTE: The +direct+ method can't be used inside of a scope block such as
-
# +namespace+ or +scope+ and will raise an error if it detects that it is.
-
1
def direct(name, options = {}, &block)
-
2
unless @scope.root?
-
raise RuntimeError, "The direct method can't be used inside a routes scope block"
-
end
-
-
2
@set.add_url_helper(name, options, &block)
-
end
-
-
# Define custom polymorphic mappings of models to URLs. This alters the
-
# behavior of +polymorphic_url+ and consequently the behavior of
-
# +link_to+ and +form_for+ when passed a model instance, e.g:
-
#
-
# resource :basket
-
#
-
# resolve "Basket" do
-
# [:basket]
-
# end
-
#
-
# This will now generate "/basket" when a +Basket+ instance is passed to
-
# +link_to+ or +form_for+ instead of the standard "/baskets/:id".
-
#
-
# NOTE: This custom behavior only applies to simple polymorphic URLs where
-
# a single model instance is passed and not more complicated forms, e.g:
-
#
-
# # config/routes.rb
-
# resource :profile
-
# namespace :admin do
-
# resources :users
-
# end
-
#
-
# resolve("User") { [:profile] }
-
#
-
# # app/views/application/_menu.html.erb
-
# link_to "Profile", @current_user
-
# link_to "Profile", [:admin, @current_user]
-
#
-
# The first +link_to+ will generate "/profile" but the second will generate
-
# the standard polymorphic URL of "/admin/users/1".
-
#
-
# You can pass options to a polymorphic mapping - the arity for the block
-
# needs to be two as the instance is passed as the first argument, e.g:
-
#
-
# resolve "Basket", anchor: "items" do |basket, options|
-
# [:basket, options]
-
# end
-
#
-
# This generates the URL "/basket#items" because when the last item in an
-
# array passed to +polymorphic_url+ is a hash then it's treated as options
-
# to the URL helper that gets called.
-
#
-
# NOTE: The +resolve+ method can't be used inside of a scope block such as
-
# +namespace+ or +scope+ and will raise an error if it detects that it is.
-
1
def resolve(*args, &block)
-
4
unless @scope.root?
-
raise RuntimeError, "The resolve method can't be used inside a routes scope block"
-
end
-
-
4
options = args.extract_options!
-
4
args = args.flatten(1)
-
-
4
args.each do |klass|
-
4
@set.add_polymorphic_mapping(klass, options, &block)
-
end
-
end
-
end
-
-
1
class Scope # :nodoc:
-
1
OPTIONS = [:path, :shallow_path, :as, :shallow_prefix, :module,
-
:controller, :action, :path_names, :constraints,
-
:shallow, :blocks, :defaults, :via, :format, :options, :to]
-
-
1
RESOURCE_SCOPES = [:resource, :resources]
-
1
RESOURCE_METHOD_SCOPES = [:collection, :member, :new]
-
-
1
attr_reader :parent, :scope_level
-
-
1
def initialize(hash, parent = NULL, scope_level = nil)
-
8
@hash = hash
-
8
@parent = parent
-
8
@scope_level = scope_level
-
end
-
-
1
def nested?
-
scope_level == :nested
-
end
-
-
1
def null?
-
6
@hash.nil? && @parent.nil?
-
end
-
-
1
def root?
-
6
@parent.null?
-
end
-
-
1
def resources?
-
1
scope_level == :resources
-
end
-
-
1
def resource_method_scope?
-
12
RESOURCE_METHOD_SCOPES.include? scope_level
-
end
-
-
1
def action_name(name_prefix, prefix, collection_name, member_name)
-
19
case scope_level
-
when :nested
-
[name_prefix, prefix]
-
when :collection
-
[prefix, name_prefix, collection_name]
-
when :new
-
[prefix, :new, name_prefix, member_name]
-
when :member
-
[prefix, name_prefix, member_name]
-
when :root
-
[name_prefix, collection_name, prefix]
-
else
-
19
[name_prefix, member_name, prefix]
-
end
-
end
-
-
1
def resource_scope?
-
RESOURCE_SCOPES.include? scope_level
-
end
-
-
1
def options
-
OPTIONS
-
end
-
-
1
def new(hash)
-
self.class.new hash, self, scope_level
-
end
-
-
1
def new_level(level)
-
self.class.new(frame, self, level)
-
end
-
-
1
def [](key)
-
468
scope = find { |node| node.frame.key? key }
-
234
scope && scope.frame[key]
-
end
-
-
1
include Enumerable
-
-
1
def each
-
234
node = self
-
234
until node.equal? NULL
-
234
yield node
-
234
node = node.parent
-
end
-
end
-
-
235
def frame; @hash; end
-
-
1
NULL = Scope.new(nil, nil)
-
end
-
-
1
def initialize(set) #:nodoc:
-
7
@set = set
-
7
@scope = Scope.new(path_names: @set.resources_path_names)
-
7
@concerns = {}
-
end
-
-
1
include Base
-
1
include HttpHelpers
-
1
include Redirection
-
1
include Scoping
-
1
include Concerns
-
1
include Resources
-
1
include CustomUrls
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "action_dispatch/http/request"
-
1
require "active_support/core_ext/uri"
-
1
require "active_support/core_ext/array/extract_options"
-
1
require "rack/utils"
-
1
require "action_controller/metal/exceptions"
-
1
require "action_dispatch/routing/endpoint"
-
-
1
module ActionDispatch
-
1
module Routing
-
1
class Redirect < Endpoint # :nodoc:
-
1
attr_reader :status, :block
-
-
1
def initialize(status, block)
-
@status = status
-
@block = block
-
end
-
-
1
def redirect?; true; end
-
-
1
def call(env)
-
serve Request.new env
-
end
-
-
1
def serve(req)
-
uri = URI.parse(path(req.path_parameters, req))
-
-
unless uri.host
-
if relative_path?(uri.path)
-
uri.path = "#{req.script_name}/#{uri.path}"
-
elsif uri.path.empty?
-
uri.path = req.script_name.empty? ? "/" : req.script_name
-
end
-
end
-
-
uri.scheme ||= req.scheme
-
uri.host ||= req.host
-
uri.port ||= req.port unless req.standard_port?
-
-
req.commit_flash
-
-
body = %(<html><body>You are being <a href="#{ERB::Util.unwrapped_html_escape(uri.to_s)}">redirected</a>.</body></html>)
-
-
headers = {
-
"Location" => uri.to_s,
-
"Content-Type" => "text/html",
-
"Content-Length" => body.length.to_s
-
}
-
-
[ status, headers, [body] ]
-
end
-
-
1
def path(params, request)
-
block.call params, request
-
end
-
-
1
def inspect
-
"redirect(#{status})"
-
end
-
-
1
private
-
1
def relative_path?(path)
-
path && !path.empty? && path[0] != "/"
-
end
-
-
1
def escape(params)
-
Hash[params.map { |k, v| [k, Rack::Utils.escape(v)] }]
-
end
-
-
1
def escape_fragment(params)
-
Hash[params.map { |k, v| [k, Journey::Router::Utils.escape_fragment(v)] }]
-
end
-
-
1
def escape_path(params)
-
Hash[params.map { |k, v| [k, Journey::Router::Utils.escape_path(v)] }]
-
end
-
end
-
-
1
class PathRedirect < Redirect
-
1
URL_PARTS = /\A([^?]+)?(\?[^#]+)?(#.+)?\z/
-
-
1
def path(params, request)
-
if block.match(URL_PARTS)
-
path = interpolation_required?($1, params) ? $1 % escape_path(params) : $1
-
query = interpolation_required?($2, params) ? $2 % escape(params) : $2
-
fragment = interpolation_required?($3, params) ? $3 % escape_fragment(params) : $3
-
-
"#{path}#{query}#{fragment}"
-
else
-
interpolation_required?(block, params) ? block % escape(params) : block
-
end
-
end
-
-
1
def inspect
-
"redirect(#{status}, #{block})"
-
end
-
-
1
private
-
1
def interpolation_required?(string, params)
-
!params.empty? && string && string.match(/%\{\w*\}/)
-
end
-
end
-
-
1
class OptionRedirect < Redirect # :nodoc:
-
1
alias :options :block
-
-
1
def path(params, request)
-
url_options = {
-
protocol: request.protocol,
-
host: request.host,
-
port: request.optional_port,
-
path: request.path,
-
params: request.query_parameters
-
}.merge! options
-
-
if !params.empty? && url_options[:path].match(/%\{\w*\}/)
-
url_options[:path] = (url_options[:path] % escape_path(params))
-
end
-
-
unless options[:host] || options[:domain]
-
if relative_path?(url_options[:path])
-
url_options[:path] = "/#{url_options[:path]}"
-
url_options[:script_name] = request.script_name
-
elsif url_options[:path].empty?
-
url_options[:path] = request.script_name.empty? ? "/" : ""
-
url_options[:script_name] = request.script_name
-
end
-
end
-
-
ActionDispatch::Http::URL.url_for url_options
-
end
-
-
1
def inspect
-
"redirect(#{status}, #{options.map { |k, v| "#{k}: #{v}" }.join(', ')})"
-
end
-
end
-
-
1
module Redirection
-
# Redirect any path to another path:
-
#
-
# get "/stories" => redirect("/posts")
-
#
-
# This will redirect the user, while ignoring certain parts of the request, including query string, etc.
-
# <tt>/stories</tt>, <tt>/stories?foo=bar</tt>, etc all redirect to <tt>/posts</tt>.
-
#
-
# You can also use interpolation in the supplied redirect argument:
-
#
-
# get 'docs/:article', to: redirect('/wiki/%{article}')
-
#
-
# Note that if you return a path without a leading slash then the URL is prefixed with the
-
# current SCRIPT_NAME environment variable. This is typically '/' but may be different in
-
# a mounted engine or where the application is deployed to a subdirectory of a website.
-
#
-
# Alternatively you can use one of the other syntaxes:
-
#
-
# The block version of redirect allows for the easy encapsulation of any logic associated with
-
# the redirect in question. Either the params and request are supplied as arguments, or just
-
# params, depending of how many arguments your block accepts. A string is required as a
-
# return value.
-
#
-
# get 'jokes/:number', to: redirect { |params, request|
-
# path = (params[:number].to_i.even? ? "wheres-the-beef" : "i-love-lamp")
-
# "http://#{request.host_with_port}/#{path}"
-
# }
-
#
-
# Note that the +do end+ syntax for the redirect block wouldn't work, as Ruby would pass
-
# the block to +get+ instead of +redirect+. Use <tt>{ ... }</tt> instead.
-
#
-
# The options version of redirect allows you to supply only the parts of the URL which need
-
# to change, it also supports interpolation of the path similar to the first example.
-
#
-
# get 'stores/:name', to: redirect(subdomain: 'stores', path: '/%{name}')
-
# get 'stores/:name(*all)', to: redirect(subdomain: 'stores', path: '/%{name}%{all}')
-
# get '/stories', to: redirect(path: '/posts')
-
#
-
# This will redirect the user, while changing only the specified parts of the request,
-
# for example the +path+ option in the last example.
-
# <tt>/stories</tt>, <tt>/stories?foo=bar</tt>, redirect to <tt>/posts</tt> and <tt>/posts?foo=bar</tt> respectively.
-
#
-
# Finally, an object which responds to call can be supplied to redirect, allowing you to reuse
-
# common redirect routes. The call method must accept two arguments, params and request, and return
-
# a string.
-
#
-
# get 'accounts/:name' => redirect(SubdomainRedirector.new('api'))
-
#
-
1
def redirect(*args, &block)
-
options = args.extract_options!
-
status = options.delete(:status) || 301
-
path = args.shift
-
-
return OptionRedirect.new(status, options) if options.any?
-
return PathRedirect.new(status, path) if String === path
-
-
block = path if path.respond_to? :call
-
raise ArgumentError, "redirection argument not supported" unless block
-
Redirect.new status, block
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActionDispatch
-
# This is a class that abstracts away an asserted response. It purposely
-
# does not inherit from Response because it doesn't need it. That means it
-
# does not have headers or a body.
-
1
class AssertionResponse
-
1
attr_reader :code, :name
-
-
1
GENERIC_RESPONSE_CODES = { # :nodoc:
-
success: "2XX",
-
missing: "404",
-
redirect: "3XX",
-
error: "5XX"
-
}
-
-
# Accepts a specific response status code as an Integer (404) or String
-
# ('404') or a response status range as a Symbol pseudo-code (:success,
-
# indicating any 200-299 status code).
-
1
def initialize(code_or_name)
-
4
if code_or_name.is_a?(Symbol)
-
2
@name = code_or_name
-
2
@code = code_from_name(code_or_name)
-
else
-
2
@name = name_from_code(code_or_name)
-
2
@code = code_or_name
-
end
-
-
4
raise ArgumentError, "Invalid response name: #{name}" if @code.nil?
-
4
raise ArgumentError, "Invalid response code: #{code}" if @name.nil?
-
end
-
-
1
def code_and_name
-
4
"#{code}: #{name}"
-
end
-
-
1
private
-
-
1
def code_from_name(name)
-
2
GENERIC_RESPONSE_CODES[name] || Rack::Utils::SYMBOL_TO_STATUS_CODE[name]
-
end
-
-
1
def name_from_code(code)
-
2
GENERIC_RESPONSE_CODES.invert[code] || Rack::Utils::HTTP_STATUS_CODES[code]
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "rails-dom-testing"
-
-
1
module ActionDispatch
-
1
module Assertions
-
1
autoload :ResponseAssertions, "action_dispatch/testing/assertions/response"
-
1
autoload :RoutingAssertions, "action_dispatch/testing/assertions/routing"
-
-
1
extend ActiveSupport::Concern
-
-
1
include ResponseAssertions
-
1
include RoutingAssertions
-
1
include Rails::Dom::Testing::Assertions
-
-
1
def html_document
-
@html_document ||= if @response.content_type.to_s.end_with?("xml")
-
Nokogiri::XML::Document.parse(@response.body)
-
else
-
Nokogiri::HTML::Document.parse(@response.body)
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActionDispatch
-
1
module Assertions
-
# A small suite of assertions that test responses from \Rails applications.
-
1
module ResponseAssertions
-
1
RESPONSE_PREDICATES = { # :nodoc:
-
success: :successful?,
-
missing: :not_found?,
-
redirect: :redirection?,
-
error: :server_error?,
-
}
-
-
# Asserts that the response is one of the following types:
-
#
-
# * <tt>:success</tt> - Status code was in the 200-299 range
-
# * <tt>:redirect</tt> - Status code was in the 300-399 range
-
# * <tt>:missing</tt> - Status code was 404
-
# * <tt>:error</tt> - Status code was in the 500-599 range
-
#
-
# You can also pass an explicit status number like <tt>assert_response(501)</tt>
-
# or its symbolic equivalent <tt>assert_response(:not_implemented)</tt>.
-
# See Rack::Utils::SYMBOL_TO_STATUS_CODE for a full list.
-
#
-
# # Asserts that the response was a redirection
-
# assert_response :redirect
-
#
-
# # Asserts that the response code was status code 401 (unauthorized)
-
# assert_response 401
-
1
def assert_response(type, message = nil)
-
2
message ||= generate_response_message(type)
-
-
2
if RESPONSE_PREDICATES.keys.include?(type)
-
2
assert @response.send(RESPONSE_PREDICATES[type]), message
-
else
-
assert_equal AssertionResponse.new(type).code, @response.response_code, message
-
end
-
end
-
-
# Asserts that the redirection options passed in match those of the redirect called in the latest action.
-
# This match can be partial, such that <tt>assert_redirected_to(controller: "weblog")</tt> will also
-
# match the redirection of <tt>redirect_to(controller: "weblog", action: "show")</tt> and so on.
-
#
-
# # Asserts that the redirection was to the "index" action on the WeblogController
-
# assert_redirected_to controller: "weblog", action: "index"
-
#
-
# # Asserts that the redirection was to the named route login_url
-
# assert_redirected_to login_url
-
#
-
# # Asserts that the redirection was to the URL for @customer
-
# assert_redirected_to @customer
-
#
-
# # Asserts that the redirection matches the regular expression
-
# assert_redirected_to %r(\Ahttp://example.org)
-
1
def assert_redirected_to(options = {}, message = nil)
-
assert_response(:redirect, message)
-
return true if options === @response.location
-
-
redirect_is = normalize_argument_to_redirection(@response.location)
-
redirect_expected = normalize_argument_to_redirection(options)
-
-
message ||= "Expected response to be a redirect to <#{redirect_expected}> but was a redirect to <#{redirect_is}>"
-
assert_operator redirect_expected, :===, redirect_is, message
-
end
-
-
1
private
-
# Proxy to to_param if the object will respond to it.
-
1
def parameterize(value)
-
value.respond_to?(:to_param) ? value.to_param : value
-
end
-
-
1
def normalize_argument_to_redirection(fragment)
-
if Regexp === fragment
-
fragment
-
else
-
handle = @controller || ActionController::Redirecting
-
handle._compute_redirect_to_location(@request, fragment)
-
end
-
end
-
-
3
def generate_response_message(expected, actual = @response.response_code)
-
8
"Expected response to be a <#{code_with_name(expected)}>,"\
-
2
" but was a <#{code_with_name(actual)}>"
-
3
.dup.concat(location_if_redirected).concat(response_body_if_short)
-
end
-
-
1
def response_body_if_short
-
2
return "" if @response.body.size > 500
-
2
"\nResponse body: #{@response.body}"
-
end
-
-
1
def location_if_redirected
-
2
return "" unless @response.redirection? && @response.location.present?
-
location = normalize_argument_to_redirection(@response.location)
-
" redirect to <#{location}>"
-
end
-
-
1
def code_with_name(code_or_name)
-
4
if RESPONSE_PREDICATES.values.include?("#{code_or_name}?".to_sym)
-
code_or_name = RESPONSE_PREDICATES.invert["#{code_or_name}?".to_sym]
-
end
-
-
4
AssertionResponse.new(code_or_name).code_and_name
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "uri"
-
1
require "active_support/core_ext/hash/indifferent_access"
-
1
require "active_support/core_ext/string/access"
-
1
require "action_controller/metal/exceptions"
-
-
1
module ActionDispatch
-
1
module Assertions
-
# Suite of assertions to test routes generated by \Rails and the handling of requests made to them.
-
1
module RoutingAssertions
-
# Asserts that the routing of the given +path+ was handled correctly and that the parsed options (given in the +expected_options+ hash)
-
# match +path+. Basically, it asserts that \Rails recognizes the route given by +expected_options+.
-
#
-
# Pass a hash in the second argument (+path+) to specify the request method. This is useful for routes
-
# requiring a specific HTTP method. The hash should contain a :path with the incoming request path
-
# and a :method containing the required HTTP verb.
-
#
-
# # Asserts that POSTing to /items will call the create action on ItemsController
-
# assert_recognizes({controller: 'items', action: 'create'}, {path: 'items', method: :post})
-
#
-
# You can also pass in +extras+ with a hash containing URL parameters that would normally be in the query string. This can be used
-
# to assert that values in the query string will end up in the params hash correctly. To test query strings you must use the extras
-
# argument because appending the query string on the path directly will not work. For example:
-
#
-
# # Asserts that a path of '/items/list/1?view=print' returns the correct options
-
# assert_recognizes({controller: 'items', action: 'list', id: '1', view: 'print'}, 'items/list/1', { view: "print" })
-
#
-
# The +message+ parameter allows you to pass in an error message that is displayed upon failure.
-
#
-
# # Check the default route (i.e., the index action)
-
# assert_recognizes({controller: 'items', action: 'index'}, 'items')
-
#
-
# # Test a specific action
-
# assert_recognizes({controller: 'items', action: 'list'}, 'items/list')
-
#
-
# # Test an action with a parameter
-
# assert_recognizes({controller: 'items', action: 'destroy', id: '1'}, 'items/destroy/1')
-
#
-
# # Test a custom route
-
# assert_recognizes({controller: 'items', action: 'show', id: '1'}, 'view/item1')
-
1
def assert_recognizes(expected_options, path, extras = {}, msg = nil)
-
if path.is_a?(Hash) && path[:method].to_s == "all"
-
[:get, :post, :put, :delete].each do |method|
-
assert_recognizes(expected_options, path.merge(method: method), extras, msg)
-
end
-
else
-
request = recognized_request_for(path, extras, msg)
-
-
expected_options = expected_options.clone
-
-
expected_options.stringify_keys!
-
-
msg = message(msg, "") {
-
sprintf("The recognized options <%s> did not match <%s>, difference:",
-
request.path_parameters, expected_options)
-
}
-
-
assert_equal(expected_options, request.path_parameters, msg)
-
end
-
end
-
-
# Asserts that the provided options can be used to generate the provided path. This is the inverse of +assert_recognizes+.
-
# The +extras+ parameter is used to tell the request the names and values of additional request parameters that would be in
-
# a query string. The +message+ parameter allows you to specify a custom error message for assertion failures.
-
#
-
# The +defaults+ parameter is unused.
-
#
-
# # Asserts that the default action is generated for a route with no action
-
# assert_generates "/items", controller: "items", action: "index"
-
#
-
# # Tests that the list action is properly routed
-
# assert_generates "/items/list", controller: "items", action: "list"
-
#
-
# # Tests the generation of a route with a parameter
-
# assert_generates "/items/list/1", { controller: "items", action: "list", id: "1" }
-
#
-
# # Asserts that the generated route gives us our custom route
-
# assert_generates "changesets/12", { controller: 'scm', action: 'show_diff', revision: "12" }
-
1
def assert_generates(expected_path, options, defaults = {}, extras = {}, message = nil)
-
if expected_path =~ %r{://}
-
fail_on(URI::InvalidURIError, message) do
-
uri = URI.parse(expected_path)
-
expected_path = uri.path.to_s.empty? ? "/" : uri.path
-
end
-
else
-
expected_path = "/#{expected_path}" unless expected_path.first == "/"
-
end
-
# Load routes.rb if it hasn't been loaded.
-
-
options = options.clone
-
generated_path, query_string_keys = @routes.generate_extras(options, defaults)
-
found_extras = options.reject { |k, _| ! query_string_keys.include? k }
-
-
msg = message || sprintf("found extras <%s>, not <%s>", found_extras, extras)
-
assert_equal(extras, found_extras, msg)
-
-
msg = message || sprintf("The generated path <%s> did not match <%s>", generated_path,
-
expected_path)
-
assert_equal(expected_path, generated_path, msg)
-
end
-
-
# Asserts that path and options match both ways; in other words, it verifies that <tt>path</tt> generates
-
# <tt>options</tt> and then that <tt>options</tt> generates <tt>path</tt>. This essentially combines +assert_recognizes+
-
# and +assert_generates+ into one step.
-
#
-
# The +extras+ hash allows you to specify options that would normally be provided as a query string to the action. The
-
# +message+ parameter allows you to specify a custom error message to display upon failure.
-
#
-
# # Asserts a basic route: a controller with the default action (index)
-
# assert_routing '/home', controller: 'home', action: 'index'
-
#
-
# # Test a route generated with a specific controller, action, and parameter (id)
-
# assert_routing '/entries/show/23', controller: 'entries', action: 'show', id: 23
-
#
-
# # Asserts a basic route (controller + default action), with an error message if it fails
-
# assert_routing '/store', { controller: 'store', action: 'index' }, {}, {}, 'Route for store index not generated properly'
-
#
-
# # Tests a route, providing a defaults hash
-
# assert_routing 'controller/action/9', {id: "9", item: "square"}, {controller: "controller", action: "action"}, {}, {item: "square"}
-
#
-
# # Tests a route with an HTTP method
-
# assert_routing({ method: 'put', path: '/product/321' }, { controller: "product", action: "update", id: "321" })
-
1
def assert_routing(path, options, defaults = {}, extras = {}, message = nil)
-
assert_recognizes(options, path, extras, message)
-
-
controller, default_controller = options[:controller], defaults[:controller]
-
if controller && controller.include?(?/) && default_controller && default_controller.include?(?/)
-
options[:controller] = "/#{controller}"
-
end
-
-
generate_options = options.dup.delete_if { |k, _| defaults.key?(k) }
-
assert_generates(path.is_a?(Hash) ? path[:path] : path, generate_options, defaults, extras, message)
-
end
-
-
# A helper to make it easier to test different route configurations.
-
# This method temporarily replaces @routes with a new RouteSet instance.
-
#
-
# The new instance is yielded to the passed block. Typically the block
-
# will create some routes using <tt>set.draw { match ... }</tt>:
-
#
-
# with_routing do |set|
-
# set.draw do
-
# resources :users
-
# end
-
# assert_equal "/users", users_path
-
# end
-
#
-
1
def with_routing
-
old_routes, @routes = @routes, ActionDispatch::Routing::RouteSet.new
-
if defined?(@controller) && @controller
-
old_controller, @controller = @controller, @controller.clone
-
_routes = @routes
-
-
@controller.singleton_class.include(_routes.url_helpers)
-
-
if @controller.respond_to? :view_context_class
-
@controller.view_context_class = Class.new(@controller.view_context_class) do
-
include _routes.url_helpers
-
end
-
end
-
end
-
yield @routes
-
ensure
-
@routes = old_routes
-
if defined?(@controller) && @controller
-
@controller = old_controller
-
end
-
end
-
-
# ROUTES TODO: These assertions should really work in an integration context
-
1
def method_missing(selector, *args, &block)
-
if defined?(@controller) && @controller && defined?(@routes) && @routes && @routes.named_routes.route_defined?(selector)
-
@controller.send(selector, *args, &block)
-
else
-
super
-
end
-
end
-
-
1
private
-
# Recognizes the route for a given path.
-
1
def recognized_request_for(path, extras = {}, msg)
-
if path.is_a?(Hash)
-
method = path[:method]
-
path = path[:path]
-
else
-
method = :get
-
end
-
-
request = ActionController::TestRequest.create @controller.class
-
-
if path =~ %r{://}
-
fail_on(URI::InvalidURIError, msg) do
-
uri = URI.parse(path)
-
request.env["rack.url_scheme"] = uri.scheme || "http"
-
request.host = uri.host if uri.host
-
request.port = uri.port if uri.port
-
request.path = uri.path.to_s.empty? ? "/" : uri.path
-
end
-
else
-
path = "/#{path}" unless path.first == "/"
-
request.path = path
-
end
-
-
request.request_method = method if method
-
-
params = fail_on(ActionController::RoutingError, msg) do
-
@routes.recognize_path(path, method: method, extras: extras)
-
end
-
request.path_parameters = params.with_indifferent_access
-
-
request
-
end
-
-
1
def fail_on(exception_class, message)
-
yield
-
rescue exception_class => e
-
raise Minitest::Assertion, message || e.message
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "stringio"
-
1
require "uri"
-
1
require "active_support/core_ext/kernel/singleton_class"
-
1
require "active_support/core_ext/object/try"
-
1
require "rack/test"
-
1
require "minitest"
-
-
1
require "action_dispatch/testing/request_encoder"
-
-
1
module ActionDispatch
-
1
module Integration #:nodoc:
-
1
module RequestHelpers
-
# Performs a GET request with the given parameters. See ActionDispatch::Integration::Session#process
-
# for more details.
-
1
def get(path, **args)
-
2
process(:get, path, **args)
-
end
-
-
# Performs a POST request with the given parameters. See ActionDispatch::Integration::Session#process
-
# for more details.
-
1
def post(path, **args)
-
process(:post, path, **args)
-
end
-
-
# Performs a PATCH request with the given parameters. See ActionDispatch::Integration::Session#process
-
# for more details.
-
1
def patch(path, **args)
-
process(:patch, path, **args)
-
end
-
-
# Performs a PUT request with the given parameters. See ActionDispatch::Integration::Session#process
-
# for more details.
-
1
def put(path, **args)
-
process(:put, path, **args)
-
end
-
-
# Performs a DELETE request with the given parameters. See ActionDispatch::Integration::Session#process
-
# for more details.
-
1
def delete(path, **args)
-
process(:delete, path, **args)
-
end
-
-
# Performs a HEAD request with the given parameters. See ActionDispatch::Integration::Session#process
-
# for more details.
-
1
def head(path, *args)
-
process(:head, path, *args)
-
end
-
-
# Follow a single redirect response. If the last response was not a
-
# redirect, an exception will be raised. Otherwise, the redirect is
-
# performed on the location header.
-
1
def follow_redirect!
-
raise "not a redirect! #{status} #{status_message}" unless redirect?
-
get(response.location)
-
status
-
end
-
end
-
-
# An instance of this class represents a set of requests and responses
-
# performed sequentially by a test process. Because you can instantiate
-
# multiple sessions and run them side-by-side, you can also mimic (to some
-
# limited extent) multiple simultaneous users interacting with your system.
-
#
-
# Typically, you will instantiate a new session using
-
# IntegrationTest#open_session, rather than instantiating
-
# Integration::Session directly.
-
1
class Session
-
1
DEFAULT_HOST = "www.example.com"
-
-
1
include Minitest::Assertions
-
1
include TestProcess, RequestHelpers, Assertions
-
-
1
%w( status status_message headers body redirect? ).each do |method|
-
5
delegate method, to: :response, allow_nil: true
-
end
-
-
1
%w( path ).each do |method|
-
1
delegate method, to: :request, allow_nil: true
-
end
-
-
# The hostname used in the last request.
-
1
def host
-
8
@host || DEFAULT_HOST
-
end
-
1
attr_writer :host
-
-
# The remote_addr used in the last request.
-
1
attr_accessor :remote_addr
-
-
# The Accept header to send.
-
1
attr_accessor :accept
-
-
# A map of the cookies returned by the last response, and which will be
-
# sent with the next request.
-
1
def cookies
-
_mock_session.cookie_jar
-
end
-
-
# A reference to the controller instance used by the last request.
-
1
attr_reader :controller
-
-
# A reference to the request instance used by the last request.
-
1
attr_reader :request
-
-
# A reference to the response instance used by the last request.
-
1
attr_reader :response
-
-
# A running counter of the number of requests processed.
-
1
attr_accessor :request_count
-
-
1
include ActionDispatch::Routing::UrlFor
-
-
# Create and initialize a new Session instance.
-
1
def initialize(app)
-
2
super()
-
2
@app = app
-
-
2
reset!
-
end
-
-
1
def url_options
-
2
@url_options ||= default_url_options.dup.tap do |url_options|
-
2
url_options.reverse_merge!(controller.url_options) if controller
-
-
2
if @app.respond_to?(:routes)
-
2
url_options.reverse_merge!(@app.routes.default_url_options)
-
end
-
-
2
url_options.reverse_merge!(host: host, protocol: https? ? "https" : "http")
-
end
-
end
-
-
# Resets the instance. This can be used to reset the state information
-
# in an existing session instance, so it can be used from a clean-slate
-
# condition.
-
#
-
# session.reset!
-
1
def reset!
-
2
@https = false
-
2
@controller = @request = @response = nil
-
2
@_mock_session = nil
-
2
@request_count = 0
-
2
@url_options = nil
-
-
2
self.host = DEFAULT_HOST
-
2
self.remote_addr = "127.0.0.1"
-
2
self.accept = "text/xml,application/xml,application/xhtml+xml," \
-
"text/html;q=0.9,text/plain;q=0.8,image/png," \
-
"*/*;q=0.5"
-
-
2
unless defined? @named_routes_configured
-
# the helpers are made protected by default--we make them public for
-
# easier access during testing and troubleshooting.
-
2
@named_routes_configured = true
-
end
-
end
-
-
# Specify whether or not the session should mimic a secure HTTPS request.
-
#
-
# session.https!
-
# session.https!(false)
-
1
def https!(flag = true)
-
2
@https = flag
-
end
-
-
# Returns +true+ if the session is mimicking a secure HTTPS request.
-
#
-
# if session.https?
-
# ...
-
# end
-
1
def https?
-
8
@https
-
end
-
-
# Performs the actual request.
-
#
-
# - +method+: The HTTP method (GET, POST, PATCH, PUT, DELETE, HEAD, OPTIONS)
-
# as a symbol.
-
# - +path+: The URI (as a String) on which you want to perform the
-
# request.
-
# - +params+: The HTTP parameters that you want to pass. This may
-
# be +nil+,
-
# a Hash, or a String that is appropriately encoded
-
# (<tt>application/x-www-form-urlencoded</tt> or
-
# <tt>multipart/form-data</tt>).
-
# - +headers+: Additional headers to pass, as a Hash. The headers will be
-
# merged into the Rack env hash.
-
# - +env+: Additional env to pass, as a Hash. The headers will be
-
# merged into the Rack env hash.
-
#
-
# This method is rarely used directly. Use +#get+, +#post+, or other standard
-
# HTTP methods in integration tests. +#process+ is only required when using a
-
# request method that doesn't have a method defined in the integration tests.
-
#
-
# This method returns the response status, after performing the request.
-
# Furthermore, if this method was called from an ActionDispatch::IntegrationTest object,
-
# then that object's <tt>@response</tt> instance variable will point to a Response object
-
# which one can use to inspect the details of the response.
-
#
-
# Example:
-
# process :get, '/author', params: { since: 201501011400 }
-
1
def process(method, path, params: nil, headers: nil, env: nil, xhr: false, as: nil)
-
2
request_encoder = RequestEncoder.encoder(as)
-
2
headers ||= {}
-
-
2
if method == :get && as == :json && params
-
headers["X-Http-Method-Override"] = "GET"
-
method = :post
-
end
-
-
2
if path =~ %r{://}
-
2
path = build_expanded_path(path) do |location|
-
2
https! URI::HTTPS === location if location.scheme
-
-
2
if url_host = location.host
-
2
default = Rack::Request::DEFAULT_PORTS[location.scheme]
-
2
url_host += ":#{location.port}" if default != location.port
-
2
host! url_host
-
end
-
end
-
end
-
-
2
hostname, port = host.split(":")
-
-
request_env = {
-
:method => method,
-
1
:params => request_encoder.encode_params(params),
-
-
"SERVER_NAME" => hostname,
-
2
"SERVER_PORT" => port || (https? ? "443" : "80"),
-
1
"HTTPS" => https? ? "on" : "off",
-
1
"rack.url_scheme" => https? ? "https" : "http",
-
-
"REQUEST_URI" => path,
-
1
"HTTP_HOST" => host,
-
1
"REMOTE_ADDR" => remote_addr,
-
1
"CONTENT_TYPE" => request_encoder.content_type,
-
1
"HTTP_ACCEPT" => request_encoder.accept_header || accept
-
}
-
-
2
wrapped_headers = Http::Headers.from_hash({})
-
2
wrapped_headers.merge!(headers) if headers
-
-
2
if xhr
-
wrapped_headers["HTTP_X_REQUESTED_WITH"] = "XMLHttpRequest"
-
wrapped_headers["HTTP_ACCEPT"] ||= [Mime[:js], Mime[:html], Mime[:xml], "text/xml", "*/*"].join(", ")
-
end
-
-
# This modifies the passed request_env directly.
-
2
if wrapped_headers.present?
-
2
Http::Headers.from_hash(request_env).merge!(wrapped_headers)
-
end
-
2
if env.present?
-
Http::Headers.from_hash(request_env).merge!(env)
-
end
-
-
2
session = Rack::Test::Session.new(_mock_session)
-
-
# NOTE: rack-test v0.5 doesn't build a default uri correctly
-
# Make sure requested path is always a full URI.
-
2
session.request(build_full_uri(path, request_env), request_env)
-
-
2
@request_count += 1
-
2
@request = ActionDispatch::Request.new(session.last_request.env)
-
2
response = _mock_session.last_response
-
2
@response = ActionDispatch::TestResponse.from_response(response)
-
2
@response.request = @request
-
2
@html_document = nil
-
2
@url_options = nil
-
-
2
@controller = @request.controller_instance
-
-
2
response.status
-
end
-
-
# Set the host name to use in the next request.
-
#
-
# session.host! "www.example.com"
-
1
alias :host! :host=
-
-
1
private
-
1
def _mock_session
-
4
@_mock_session ||= Rack::MockSession.new(@app, host)
-
end
-
-
1
def build_full_uri(path, env)
-
2
"#{env['rack.url_scheme']}://#{env['SERVER_NAME']}:#{env['SERVER_PORT']}#{path}"
-
end
-
-
1
def build_expanded_path(path)
-
2
location = URI.parse(path)
-
2
yield location if block_given?
-
2
path = location.path
-
2
location.query ? "#{path}?#{location.query}" : path
-
end
-
end
-
-
1
module Runner
-
1
include ActionDispatch::Assertions
-
-
1
APP_SESSIONS = {}
-
-
1
attr_reader :app
-
-
1
def initialize(*args, &blk)
-
2
super(*args, &blk)
-
2
@integration_session = nil
-
end
-
-
1
def before_setup # :nodoc:
-
2
@app = nil
-
2
super
-
end
-
-
1
def integration_session
-
6
@integration_session ||= create_session(app)
-
end
-
-
# Reset the current session. This is useful for testing multiple sessions
-
# in a single test case.
-
1
def reset!
-
@integration_session = create_session(app)
-
end
-
-
1
def create_session(app)
-
2
klass = APP_SESSIONS[app] ||= Class.new(Integration::Session) {
-
# If the app is a Rails app, make url_helpers available on the session.
-
# This makes app.url_for and app.foo_path available in the console.
-
1
if app.respond_to?(:routes)
-
1
include app.routes.url_helpers
-
1
include app.routes.mounted_helpers
-
end
-
}
-
2
klass.new(app)
-
end
-
-
1
def remove! # :nodoc:
-
@integration_session = nil
-
end
-
-
1
%w(get post patch put head delete cookies assigns follow_redirect!).each do |method|
-
9
define_method(method) do |*args|
-
# reset the html_document variable, except for cookies/assigns calls
-
2
unless method == "cookies" || method == "assigns"
-
2
@html_document = nil
-
end
-
-
2
integration_session.__send__(method, *args).tap do
-
2
copy_session_variables!
-
end
-
end
-
end
-
-
# Open a new session instance. If a block is given, the new session is
-
# yielded to the block before being returned.
-
#
-
# session = open_session do |sess|
-
# sess.extend(CustomAssertions)
-
# end
-
#
-
# By default, a single session is automatically created for you, but you
-
# can use this method to open multiple sessions that ought to be tested
-
# simultaneously.
-
1
def open_session
-
dup.tap do |session|
-
session.reset!
-
yield session if block_given?
-
end
-
end
-
-
# Copy the instance variables from the current session instance into the
-
# test instance.
-
1
def copy_session_variables! #:nodoc:
-
4
@controller = @integration_session.controller
-
4
@response = @integration_session.response
-
4
@request = @integration_session.request
-
end
-
-
1
def default_url_options
-
integration_session.default_url_options
-
end
-
-
1
def default_url_options=(options)
-
integration_session.default_url_options = options
-
end
-
-
1
private
-
1
def respond_to_missing?(method, _)
-
integration_session.respond_to?(method) || super
-
end
-
-
# Delegate unhandled messages to the current session instance.
-
1
def method_missing(method, *args, &block)
-
2
if integration_session.respond_to?(method)
-
2
integration_session.public_send(method, *args, &block).tap do
-
2
copy_session_variables!
-
end
-
else
-
super
-
end
-
end
-
end
-
end
-
-
# An integration test spans multiple controllers and actions,
-
# tying them all together to ensure they work together as expected. It tests
-
# more completely than either unit or functional tests do, exercising the
-
# entire stack, from the dispatcher to the database.
-
#
-
# At its simplest, you simply extend <tt>IntegrationTest</tt> and write your tests
-
# using the get/post methods:
-
#
-
# require "test_helper"
-
#
-
# class ExampleTest < ActionDispatch::IntegrationTest
-
# fixtures :people
-
#
-
# def test_login
-
# # get the login page
-
# get "/login"
-
# assert_equal 200, status
-
#
-
# # post the login and follow through to the home page
-
# post "/login", params: { username: people(:jamis).username,
-
# password: people(:jamis).password }
-
# follow_redirect!
-
# assert_equal 200, status
-
# assert_equal "/home", path
-
# end
-
# end
-
#
-
# However, you can also have multiple session instances open per test, and
-
# even extend those instances with assertions and methods to create a very
-
# powerful testing DSL that is specific for your application. You can even
-
# reference any named routes you happen to have defined.
-
#
-
# require "test_helper"
-
#
-
# class AdvancedTest < ActionDispatch::IntegrationTest
-
# fixtures :people, :rooms
-
#
-
# def test_login_and_speak
-
# jamis, david = login(:jamis), login(:david)
-
# room = rooms(:office)
-
#
-
# jamis.enter(room)
-
# jamis.speak(room, "anybody home?")
-
#
-
# david.enter(room)
-
# david.speak(room, "hello!")
-
# end
-
#
-
# private
-
#
-
# module CustomAssertions
-
# def enter(room)
-
# # reference a named route, for maximum internal consistency!
-
# get(room_url(id: room.id))
-
# assert(...)
-
# ...
-
# end
-
#
-
# def speak(room, message)
-
# post "/say/#{room.id}", xhr: true, params: { message: message }
-
# assert(...)
-
# ...
-
# end
-
# end
-
#
-
# def login(who)
-
# open_session do |sess|
-
# sess.extend(CustomAssertions)
-
# who = people(who)
-
# sess.post "/login", params: { username: who.username,
-
# password: who.password }
-
# assert(...)
-
# end
-
# end
-
# end
-
#
-
# Another longer example would be:
-
#
-
# A simple integration test that exercises multiple controllers:
-
#
-
# require 'test_helper'
-
#
-
# class UserFlowsTest < ActionDispatch::IntegrationTest
-
# test "login and browse site" do
-
# # login via https
-
# https!
-
# get "/login"
-
# assert_response :success
-
#
-
# post "/login", params: { username: users(:david).username, password: users(:david).password }
-
# follow_redirect!
-
# assert_equal '/welcome', path
-
# assert_equal 'Welcome david!', flash[:notice]
-
#
-
# https!(false)
-
# get "/articles/all"
-
# assert_response :success
-
# assert_select 'h1', 'Articles'
-
# end
-
# end
-
#
-
# As you can see the integration test involves multiple controllers and
-
# exercises the entire stack from database to dispatcher. In addition you can
-
# have multiple session instances open simultaneously in a test and extend
-
# those instances with assertion methods to create a very powerful testing
-
# DSL (domain-specific language) just for your application.
-
#
-
# Here's an example of multiple sessions and custom DSL in an integration test
-
#
-
# require 'test_helper'
-
#
-
# class UserFlowsTest < ActionDispatch::IntegrationTest
-
# test "login and browse site" do
-
# # User david logs in
-
# david = login(:david)
-
# # User guest logs in
-
# guest = login(:guest)
-
#
-
# # Both are now available in different sessions
-
# assert_equal 'Welcome david!', david.flash[:notice]
-
# assert_equal 'Welcome guest!', guest.flash[:notice]
-
#
-
# # User david can browse site
-
# david.browses_site
-
# # User guest can browse site as well
-
# guest.browses_site
-
#
-
# # Continue with other assertions
-
# end
-
#
-
# private
-
#
-
# module CustomDsl
-
# def browses_site
-
# get "/products/all"
-
# assert_response :success
-
# assert_select 'h1', 'Products'
-
# end
-
# end
-
#
-
# def login(user)
-
# open_session do |sess|
-
# sess.extend(CustomDsl)
-
# u = users(user)
-
# sess.https!
-
# sess.post "/login", params: { username: u.username, password: u.password }
-
# assert_equal '/welcome', sess.path
-
# sess.https!(false)
-
# end
-
# end
-
# end
-
#
-
# See the {request helpers documentation}[rdoc-ref:ActionDispatch::Integration::RequestHelpers] for help on how to
-
# use +get+, etc.
-
#
-
# === Changing the request encoding
-
#
-
# You can also test your JSON API easily by setting what the request should
-
# be encoded as:
-
#
-
# require "test_helper"
-
#
-
# class ApiTest < ActionDispatch::IntegrationTest
-
# test "creates articles" do
-
# assert_difference -> { Article.count } do
-
# post articles_path, params: { article: { title: "Ahoy!" } }, as: :json
-
# end
-
#
-
# assert_response :success
-
# assert_equal({ id: Article.last.id, title: "Ahoy!" }, response.parsed_body)
-
# end
-
# end
-
#
-
# The +as+ option passes an "application/json" Accept header (thereby setting
-
# the request format to JSON unless overridden), sets the content type to
-
# "application/json" and encodes the parameters as JSON.
-
#
-
# Calling +parsed_body+ on the response parses the response body based on the
-
# last response MIME type.
-
#
-
# Out of the box, only <tt>:json</tt> is supported. But for any custom MIME
-
# types you've registered, you can add your own encoders with:
-
#
-
# ActionDispatch::IntegrationTest.register_encoder :wibble,
-
# param_encoder: -> params { params.to_wibble },
-
# response_parser: -> body { body }
-
#
-
# Where +param_encoder+ defines how the params should be encoded and
-
# +response_parser+ defines how the response body should be parsed through
-
# +parsed_body+.
-
#
-
# Consult the Rails Testing Guide for more.
-
-
1
class IntegrationTest < ActiveSupport::TestCase
-
1
include TestProcess::FixtureFile
-
-
1
module UrlOptions
-
1
extend ActiveSupport::Concern
-
1
def url_options
-
integration_session.url_options
-
end
-
end
-
-
1
module Behavior
-
1
extend ActiveSupport::Concern
-
-
1
include Integration::Runner
-
1
include ActionController::TemplateAssertions
-
-
1
included do
-
1
include ActionDispatch::Routing::UrlFor
-
1
include UrlOptions # don't let UrlFor override the url_options method
-
1
ActiveSupport.run_load_hooks(:action_dispatch_integration_test, self)
-
1
@@app = nil
-
end
-
-
1
module ClassMethods
-
1
def app
-
2
if defined?(@@app) && @@app
-
@@app
-
else
-
2
ActionDispatch.test_app
-
end
-
end
-
-
1
def app=(app)
-
@@app = app
-
end
-
-
1
def register_encoder(*args)
-
RequestEncoder.register_encoder(*args)
-
end
-
end
-
-
1
def app
-
2
super || self.class.app
-
end
-
-
1
def document_root_element
-
html_document.root
-
end
-
end
-
-
1
include Behavior
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActionDispatch
-
1
class RequestEncoder # :nodoc:
-
1
class IdentityEncoder
-
1
def content_type; end
-
1
def accept_header; end
-
3
def encode_params(params); params; end
-
1
def response_parser; -> body { body }; end
-
end
-
-
1
@encoders = { identity: IdentityEncoder.new }
-
-
1
attr_reader :response_parser
-
-
1
def initialize(mime_name, param_encoder, response_parser)
-
1
@mime = Mime[mime_name]
-
-
1
unless @mime
-
raise ArgumentError, "Can't register a request encoder for " \
-
"unregistered MIME Type: #{mime_name}. See `Mime::Type.register`."
-
end
-
-
1
@response_parser = response_parser || -> body { body }
-
1
@param_encoder = param_encoder || :"to_#{@mime.symbol}".to_proc
-
end
-
-
1
def content_type
-
@mime.to_s
-
end
-
-
1
def accept_header
-
@mime.to_s
-
end
-
-
1
def encode_params(params)
-
@param_encoder.call(params) if params
-
end
-
-
1
def self.parser(content_type)
-
2
mime = Mime::Type.lookup(content_type)
-
2
encoder(mime ? mime.ref : nil).response_parser
-
end
-
-
1
def self.encoder(name)
-
4
@encoders[name] || @encoders[:identity]
-
end
-
-
1
def self.register_encoder(mime_name, param_encoder: nil, response_parser: nil)
-
1
@encoders[mime_name] = new(mime_name, param_encoder, response_parser)
-
end
-
-
1
register_encoder :json, response_parser: -> body { JSON.parse(body) }
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "action_dispatch/middleware/cookies"
-
1
require "action_dispatch/middleware/flash"
-
-
1
module ActionDispatch
-
1
module TestProcess
-
1
module FixtureFile
-
# Shortcut for <tt>Rack::Test::UploadedFile.new(File.join(ActionDispatch::IntegrationTest.fixture_path, path), type)</tt>:
-
#
-
# post :change_avatar, avatar: fixture_file_upload('files/spongebob.png', 'image/png')
-
#
-
# To upload binary files on Windows, pass <tt>:binary</tt> as the last parameter.
-
# This will not affect other platforms:
-
#
-
# post :change_avatar, avatar: fixture_file_upload('files/spongebob.png', 'image/png', :binary)
-
1
def fixture_file_upload(path, mime_type = nil, binary = false)
-
if self.class.respond_to?(:fixture_path) && self.class.fixture_path &&
-
!File.exist?(path)
-
path = File.join(self.class.fixture_path, path)
-
end
-
Rack::Test::UploadedFile.new(path, mime_type, binary)
-
end
-
end
-
-
1
include FixtureFile
-
-
1
def assigns(key = nil)
-
raise NoMethodError,
-
"assigns has been extracted to a gem. To continue using it,
-
add `gem 'rails-controller-testing'` to your Gemfile."
-
end
-
-
1
def session
-
@request.session
-
end
-
-
1
def flash
-
@request.flash
-
end
-
-
1
def cookies
-
@cookie_jar ||= Cookies::CookieJar.build(@request, @request.cookies)
-
end
-
-
1
def redirect_to_url
-
@response.redirect_url
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_support/core_ext/hash/indifferent_access"
-
1
require "rack/utils"
-
-
1
module ActionDispatch
-
1
class TestRequest < Request
-
1
DEFAULT_ENV = Rack::MockRequest.env_for("/",
-
"HTTP_HOST" => "test.host",
-
"REMOTE_ADDR" => "0.0.0.0",
-
"HTTP_USER_AGENT" => "Rails Testing",
-
)
-
-
# Create a new test request with default +env+ values.
-
1
def self.create(env = {})
-
env = Rails.application.env_config.merge(env) if defined?(Rails.application) && Rails.application
-
env["rack.request.cookie_hash"] ||= {}.with_indifferent_access
-
new(default_env.merge(env))
-
end
-
-
1
def self.default_env
-
DEFAULT_ENV
-
end
-
1
private_class_method :default_env
-
-
1
def request_method=(method)
-
super(method.to_s.upcase)
-
end
-
-
1
def host=(host)
-
set_header("HTTP_HOST", host)
-
end
-
-
1
def port=(number)
-
set_header("SERVER_PORT", number.to_i)
-
end
-
-
1
def request_uri=(uri)
-
set_header("REQUEST_URI", uri)
-
end
-
-
1
def path=(path)
-
set_header("PATH_INFO", path)
-
end
-
-
1
def action=(action_name)
-
path_parameters[:action] = action_name.to_s
-
end
-
-
1
def if_modified_since=(last_modified)
-
set_header("HTTP_IF_MODIFIED_SINCE", last_modified)
-
end
-
-
1
def if_none_match=(etag)
-
set_header("HTTP_IF_NONE_MATCH", etag)
-
end
-
-
1
def remote_addr=(addr)
-
set_header("REMOTE_ADDR", addr)
-
end
-
-
1
def user_agent=(user_agent)
-
set_header("HTTP_USER_AGENT", user_agent)
-
end
-
-
1
def accept=(mime_types)
-
delete_header("action_dispatch.request.accepts")
-
set_header("HTTP_ACCEPT", Array(mime_types).collect(&:to_s).join(","))
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "action_dispatch/testing/request_encoder"
-
-
1
module ActionDispatch
-
# Integration test methods such as ActionDispatch::Integration::Session#get
-
# and ActionDispatch::Integration::Session#post return objects of class
-
# TestResponse, which represent the HTTP response results of the requested
-
# controller actions.
-
#
-
# See Response for more information on controller response objects.
-
1
class TestResponse < Response
-
1
def self.from_response(response)
-
2
new response.status, response.headers, response.body
-
end
-
-
1
def initialize(*) # :nodoc:
-
2
super
-
2
@response_parser = RequestEncoder.parser(content_type)
-
end
-
-
# Was the response successful?
-
1
def success?
-
ActiveSupport::Deprecation.warn(<<-MSG.squish)
-
The success? predicate is deprecated and will be removed in Rails 6.0.
-
Please use successful? as provided by Rack::Response::Helpers.
-
MSG
-
successful?
-
end
-
-
# Was the URL not found?
-
1
def missing?
-
ActiveSupport::Deprecation.warn(<<-MSG.squish)
-
The missing? predicate is deprecated and will be removed in Rails 6.0.
-
Please use not_found? as provided by Rack::Response::Helpers.
-
MSG
-
not_found?
-
end
-
-
# Was there a server-side error?
-
1
def error?
-
ActiveSupport::Deprecation.warn(<<-MSG.squish)
-
The error? predicate is deprecated and will be removed in Rails 6.0.
-
Please use server_error? as provided by Rack::Response::Helpers.
-
MSG
-
server_error?
-
end
-
-
1
def parsed_body
-
@parsed_body ||= @response_parser.call(body)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "concurrent/map"
-
1
require "action_view/path_set"
-
-
1
module ActionView
-
1
class DependencyTracker # :nodoc:
-
1
@trackers = Concurrent::Map.new
-
-
1
def self.find_dependencies(name, template, view_paths = nil)
-
tracker = @trackers[template.handler]
-
return [] unless tracker
-
-
tracker.call(name, template, view_paths)
-
end
-
-
1
def self.register_tracker(extension, tracker)
-
2
handler = Template.handler_for_extension(extension)
-
2
if tracker.respond_to?(:supports_view_paths?)
-
2
@trackers[handler] = tracker
-
else
-
@trackers[handler] = lambda { |name, template, _|
-
tracker.call(name, template)
-
}
-
end
-
end
-
-
1
def self.remove_tracker(handler)
-
@trackers.delete(handler)
-
end
-
-
1
class ERBTracker # :nodoc:
-
1
EXPLICIT_DEPENDENCY = /# Template Dependency: (\S+)/
-
-
# A valid ruby identifier - suitable for class, method and specially variable names
-
1
IDENTIFIER = /
-
[[:alpha:]_] # at least one uppercase letter, lowercase letter or underscore
-
[[:word:]]* # followed by optional letters, numbers or underscores
-
/x
-
-
# Any kind of variable name. e.g. @instance, @@class, $global or local.
-
# Possibly following a method call chain
-
VARIABLE_OR_METHOD_CHAIN = /
-
(?:\$|@{1,2})? # optional global, instance or class variable indicator
-
1
(?:#{IDENTIFIER}\.)* # followed by an optional chain of zero-argument method calls
-
1
(?<dynamic>#{IDENTIFIER}) # and a final valid identifier, captured as DYNAMIC
-
/x
-
-
# A simple string literal. e.g. "School's out!"
-
1
STRING = /
-
(?<quote>['"]) # an opening quote
-
(?<static>.*?) # with anything inside, captured as STATIC
-
\k<quote> # and a matching closing quote
-
/x
-
-
# Part of any hash containing the :partial key
-
1
PARTIAL_HASH_KEY = /
-
(?:\bpartial:|:partial\s*=>) # partial key in either old or new style hash syntax
-
\s* # followed by optional spaces
-
/x
-
-
# Part of any hash containing the :layout key
-
1
LAYOUT_HASH_KEY = /
-
(?:\blayout:|:layout\s*=>) # layout key in either old or new style hash syntax
-
\s* # followed by optional spaces
-
/x
-
-
# Matches:
-
# partial: "comments/comment", collection: @all_comments => "comments/comment"
-
# (object: @single_comment, partial: "comments/comment") => "comments/comment"
-
#
-
# "comments/comments"
-
# 'comments/comments'
-
# ('comments/comments')
-
#
-
# (@topic) => "topics/topic"
-
# topics => "topics/topic"
-
# (message.topics) => "topics/topic"
-
RENDER_ARGUMENTS = /\A
-
(?:\s*\(?\s*) # optional opening paren surrounded by spaces
-
1
(?:.*?#{PARTIAL_HASH_KEY}|#{LAYOUT_HASH_KEY})? # optional hash, up to the partial or layout key declaration
-
1
(?:#{STRING}|#{VARIABLE_OR_METHOD_CHAIN}) # finally, the dependency name of interest
-
/xm
-
-
LAYOUT_DEPENDENCY = /\A
-
(?:\s*\(?\s*) # optional opening paren surrounded by spaces
-
1
(?:.*?#{LAYOUT_HASH_KEY}) # check if the line has layout key declaration
-
1
(?:#{STRING}|#{VARIABLE_OR_METHOD_CHAIN}) # finally, the dependency name of interest
-
/xm
-
-
1
def self.supports_view_paths? # :nodoc:
-
true
-
end
-
-
1
def self.call(name, template, view_paths = nil)
-
new(name, template, view_paths).dependencies
-
end
-
-
1
def initialize(name, template, view_paths = nil)
-
@name, @template, @view_paths = name, template, view_paths
-
end
-
-
1
def dependencies
-
render_dependencies + explicit_dependencies
-
end
-
-
1
attr_reader :name, :template
-
1
private :name, :template
-
-
1
private
-
1
def source
-
template.source
-
end
-
-
1
def directory
-
name.split("/")[0..-2].join("/")
-
end
-
-
1
def render_dependencies
-
render_dependencies = []
-
render_calls = source.split(/\brender\b/).drop(1)
-
-
render_calls.each do |arguments|
-
add_dependencies(render_dependencies, arguments, LAYOUT_DEPENDENCY)
-
add_dependencies(render_dependencies, arguments, RENDER_ARGUMENTS)
-
end
-
-
render_dependencies.uniq
-
end
-
-
1
def add_dependencies(render_dependencies, arguments, pattern)
-
arguments.scan(pattern) do
-
add_dynamic_dependency(render_dependencies, Regexp.last_match[:dynamic])
-
add_static_dependency(render_dependencies, Regexp.last_match[:static])
-
end
-
end
-
-
1
def add_dynamic_dependency(dependencies, dependency)
-
if dependency
-
dependencies << "#{dependency.pluralize}/#{dependency.singularize}"
-
end
-
end
-
-
1
def add_static_dependency(dependencies, dependency)
-
if dependency
-
if dependency.include?("/")
-
dependencies << dependency
-
else
-
dependencies << "#{directory}/#{dependency}"
-
end
-
end
-
end
-
-
1
def resolve_directories(wildcard_dependencies)
-
return [] unless @view_paths
-
-
wildcard_dependencies.flat_map { |query, templates|
-
@view_paths.find_all_with_query(query).map do |template|
-
"#{File.dirname(query)}/#{File.basename(template).split('.').first}"
-
end
-
}.sort
-
end
-
-
1
def explicit_dependencies
-
dependencies = source.scan(EXPLICIT_DEPENDENCY).flatten.uniq
-
-
wildcards, explicits = dependencies.partition { |dependency| dependency[-1] == "*" }
-
-
(explicits + resolve_directories(wildcards)).uniq
-
end
-
end
-
-
1
register_tracker :erb, ERBTracker
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "concurrent/map"
-
1
require "action_view/dependency_tracker"
-
1
require "monitor"
-
-
1
module ActionView
-
1
class Digestor
-
1
@@digest_mutex = Mutex.new
-
-
1
module PerExecutionDigestCacheExpiry
-
1
def self.before(target)
-
2
ActionView::LookupContext::DetailsKey.clear
-
end
-
end
-
-
1
class << self
-
# Supported options:
-
#
-
# * <tt>name</tt> - Template name
-
# * <tt>finder</tt> - An instance of <tt>ActionView::LookupContext</tt>
-
# * <tt>dependencies</tt> - An array of dependent views
-
1
def digest(name:, finder:, dependencies: [])
-
dependencies ||= []
-
cache_key = [ name, finder.rendered_format, dependencies ].flatten.compact.join(".")
-
-
# this is a correctly done double-checked locking idiom
-
# (Concurrent::Map's lookups have volatile semantics)
-
finder.digest_cache[cache_key] || @@digest_mutex.synchronize do
-
finder.digest_cache.fetch(cache_key) do # re-check under lock
-
partial = name.include?("/_")
-
root = tree(name, finder, partial)
-
dependencies.each do |injected_dep|
-
root.children << Injected.new(injected_dep, nil, nil)
-
end
-
finder.digest_cache[cache_key] = root.digest(finder)
-
end
-
end
-
end
-
-
1
def logger
-
ActionView::Base.logger || NullLogger
-
end
-
-
# Create a dependency tree for template named +name+.
-
1
def tree(name, finder, partial = false, seen = {})
-
logical_name = name.gsub(%r|/_|, "/")
-
-
if template = find_template(finder, logical_name, [], partial, [])
-
finder.rendered_format ||= template.formats.first
-
-
if node = seen[template.identifier] # handle cycles in the tree
-
node
-
else
-
node = seen[template.identifier] = Node.create(name, logical_name, template, partial)
-
-
deps = DependencyTracker.find_dependencies(name, template, finder.view_paths)
-
deps.uniq { |n| n.gsub(%r|/_|, "/") }.each do |dep_file|
-
node.children << tree(dep_file, finder, true, seen)
-
end
-
node
-
end
-
else
-
unless name.include?("#") # Dynamic template partial names can never be tracked
-
logger.error " Couldn't find template for digesting: #{name}"
-
end
-
-
seen[name] ||= Missing.new(name, logical_name, nil)
-
end
-
end
-
-
1
private
-
1
def find_template(finder, name, prefixes, partial, keys)
-
finder.disable_cache do
-
format = finder.rendered_format
-
result = finder.find_all(name, prefixes, partial, keys, formats: [format]).first if format
-
result || finder.find_all(name, prefixes, partial, keys).first
-
end
-
end
-
end
-
-
1
class Node
-
1
attr_reader :name, :logical_name, :template, :children
-
-
1
def self.create(name, logical_name, template, partial)
-
klass = partial ? Partial : Node
-
klass.new(name, logical_name, template, [])
-
end
-
-
1
def initialize(name, logical_name, template, children = [])
-
@name = name
-
@logical_name = logical_name
-
@template = template
-
@children = children
-
end
-
-
1
def digest(finder, stack = [])
-
ActiveSupport::Digest.hexdigest("#{template.source}-#{dependency_digest(finder, stack)}")
-
end
-
-
1
def dependency_digest(finder, stack)
-
children.map do |node|
-
if stack.include?(node)
-
false
-
else
-
finder.digest_cache[node.name] ||= begin
-
stack.push node
-
node.digest(finder, stack).tap { stack.pop }
-
end
-
end
-
end.join("-")
-
end
-
-
1
def to_dep_map
-
children.any? ? { name => children.map(&:to_dep_map) } : name
-
end
-
end
-
-
1
class Partial < Node; end
-
-
1
class Missing < Node
-
1
def digest(finder, _ = []) "" end
-
end
-
-
1
class Injected < Node
-
1
def digest(finder, _ = []) name end
-
end
-
-
1
class NullLogger
-
1
def self.debug(_); end
-
1
def self.error(_); end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActionView
-
# This class defines the interface for a renderer. Each class that
-
# subclasses +AbstractRenderer+ is used by the base +Renderer+ class to
-
# render a specific type of object.
-
#
-
# The base +Renderer+ class uses its +render+ method to delegate to the
-
# renderers. These currently consist of
-
#
-
# PartialRenderer - Used for rendering partials
-
# TemplateRenderer - Used for rendering other types of templates
-
# StreamingTemplateRenderer - Used for streaming
-
#
-
# Whenever the +render+ method is called on the base +Renderer+ class, a new
-
# renderer object of the correct type is created, and the +render+ method on
-
# that new object is called in turn. This abstracts the setup and rendering
-
# into a separate classes for partials and templates.
-
1
class AbstractRenderer #:nodoc:
-
1
delegate :find_template, :find_file, :template_exists?, :any_templates?, :with_fallbacks, :with_layout_format, :formats, to: :@lookup_context
-
-
1
def initialize(lookup_context)
-
@lookup_context = lookup_context
-
end
-
-
1
def render
-
raise NotImplementedError
-
end
-
-
1
private
-
-
1
def extract_details(options) # :doc:
-
@lookup_context.registered_details.each_with_object({}) do |key, details|
-
value = options[key]
-
-
details[key] = Array(value) if value
-
end
-
end
-
-
1
def instrument(name, **options) # :doc:
-
options[:identifier] ||= (@template && @template.identifier) || @path
-
-
ActiveSupport::Notifications.instrument("render_#{name}.action_view", options) do |payload|
-
yield payload
-
end
-
end
-
-
1
def prepend_formats(formats) # :doc:
-
formats = Array(formats)
-
return if formats.empty? || @lookup_context.html_fallback_for_js
-
-
@lookup_context.formats = formats | @lookup_context.formats
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "concurrent/map"
-
1
require "action_view/renderer/partial_renderer/collection_caching"
-
-
1
module ActionView
-
1
class PartialIteration
-
# The number of iterations that will be done by the partial.
-
1
attr_reader :size
-
-
# The current iteration of the partial.
-
1
attr_reader :index
-
-
1
def initialize(size)
-
@size = size
-
@index = 0
-
end
-
-
# Check if this is the first iteration of the partial.
-
1
def first?
-
index == 0
-
end
-
-
# Check if this is the last iteration of the partial.
-
1
def last?
-
index == size - 1
-
end
-
-
1
def iterate! # :nodoc:
-
@index += 1
-
end
-
end
-
-
# = Action View Partials
-
#
-
# There's also a convenience method for rendering sub templates within the current controller that depends on a
-
# single object (we call this kind of sub templates for partials). It relies on the fact that partials should
-
# follow the naming convention of being prefixed with an underscore -- as to separate them from regular
-
# templates that could be rendered on their own.
-
#
-
# In a template for Advertiser#account:
-
#
-
# <%= render partial: "account" %>
-
#
-
# This would render "advertiser/_account.html.erb".
-
#
-
# In another template for Advertiser#buy, we could have:
-
#
-
# <%= render partial: "account", locals: { account: @buyer } %>
-
#
-
# <% @advertisements.each do |ad| %>
-
# <%= render partial: "ad", locals: { ad: ad } %>
-
# <% end %>
-
#
-
# This would first render <tt>advertiser/_account.html.erb</tt> with <tt>@buyer</tt> passed in as the local variable +account+, then
-
# render <tt>advertiser/_ad.html.erb</tt> and pass the local variable +ad+ to the template for display.
-
#
-
# == The :as and :object options
-
#
-
# By default ActionView::PartialRenderer doesn't have any local variables.
-
# The <tt>:object</tt> option can be used to pass an object to the partial. For instance:
-
#
-
# <%= render partial: "account", object: @buyer %>
-
#
-
# would provide the <tt>@buyer</tt> object to the partial, available under the local variable +account+ and is
-
# equivalent to:
-
#
-
# <%= render partial: "account", locals: { account: @buyer } %>
-
#
-
# With the <tt>:as</tt> option we can specify a different name for said local variable. For example, if we
-
# wanted it to be +user+ instead of +account+ we'd do:
-
#
-
# <%= render partial: "account", object: @buyer, as: 'user' %>
-
#
-
# This is equivalent to
-
#
-
# <%= render partial: "account", locals: { user: @buyer } %>
-
#
-
# == \Rendering a collection of partials
-
#
-
# The example of partial use describes a familiar pattern where a template needs to iterate over an array and
-
# render a sub template for each of the elements. This pattern has been implemented as a single method that
-
# accepts an array and renders a partial by the same name as the elements contained within. So the three-lined
-
# example in "Using partials" can be rewritten with a single line:
-
#
-
# <%= render partial: "ad", collection: @advertisements %>
-
#
-
# This will render <tt>advertiser/_ad.html.erb</tt> and pass the local variable +ad+ to the template for display. An
-
# iteration object will automatically be made available to the template with a name of the form
-
# +partial_name_iteration+. The iteration object has knowledge about which index the current object has in
-
# the collection and the total size of the collection. The iteration object also has two convenience methods,
-
# +first?+ and +last?+. In the case of the example above, the template would be fed +ad_iteration+.
-
# For backwards compatibility the +partial_name_counter+ is still present and is mapped to the iteration's
-
# +index+ method.
-
#
-
# The <tt>:as</tt> option may be used when rendering partials.
-
#
-
# You can specify a partial to be rendered between elements via the <tt>:spacer_template</tt> option.
-
# The following example will render <tt>advertiser/_ad_divider.html.erb</tt> between each ad partial:
-
#
-
# <%= render partial: "ad", collection: @advertisements, spacer_template: "ad_divider" %>
-
#
-
# If the given <tt>:collection</tt> is +nil+ or empty, <tt>render</tt> will return +nil+. This will allow you
-
# to specify a text which will be displayed instead by using this form:
-
#
-
# <%= render(partial: "ad", collection: @advertisements) || "There's no ad to be displayed" %>
-
#
-
# NOTE: Due to backwards compatibility concerns, the collection can't be one of hashes. Normally you'd also
-
# just keep domain objects, like Active Records, in there.
-
#
-
# == \Rendering shared partials
-
#
-
# Two controllers can share a set of partials and render them like this:
-
#
-
# <%= render partial: "advertisement/ad", locals: { ad: @advertisement } %>
-
#
-
# This will render the partial <tt>advertisement/_ad.html.erb</tt> regardless of which controller this is being called from.
-
#
-
# == \Rendering objects that respond to +to_partial_path+
-
#
-
# Instead of explicitly naming the location of a partial, you can also let PartialRenderer do the work
-
# and pick the proper path by checking +to_partial_path+ method.
-
#
-
# # @account.to_partial_path returns 'accounts/account', so it can be used to replace:
-
# # <%= render partial: "accounts/account", locals: { account: @account} %>
-
# <%= render partial: @account %>
-
#
-
# # @posts is an array of Post instances, so every post record returns 'posts/post' on +to_partial_path+,
-
# # that's why we can replace:
-
# # <%= render partial: "posts/post", collection: @posts %>
-
# <%= render partial: @posts %>
-
#
-
# == \Rendering the default case
-
#
-
# If you're not going to be using any of the options like collections or layouts, you can also use the short-hand
-
# defaults of render to render partials. Examples:
-
#
-
# # Instead of <%= render partial: "account" %>
-
# <%= render "account" %>
-
#
-
# # Instead of <%= render partial: "account", locals: { account: @buyer } %>
-
# <%= render "account", account: @buyer %>
-
#
-
# # @account.to_partial_path returns 'accounts/account', so it can be used to replace:
-
# # <%= render partial: "accounts/account", locals: { account: @account} %>
-
# <%= render @account %>
-
#
-
# # @posts is an array of Post instances, so every post record returns 'posts/post' on +to_partial_path+,
-
# # that's why we can replace:
-
# # <%= render partial: "posts/post", collection: @posts %>
-
# <%= render @posts %>
-
#
-
# == \Rendering partials with layouts
-
#
-
# Partials can have their own layouts applied to them. These layouts are different than the ones that are
-
# specified globally for the entire action, but they work in a similar fashion. Imagine a list with two types
-
# of users:
-
#
-
# <%# app/views/users/index.html.erb %>
-
# Here's the administrator:
-
# <%= render partial: "user", layout: "administrator", locals: { user: administrator } %>
-
#
-
# Here's the editor:
-
# <%= render partial: "user", layout: "editor", locals: { user: editor } %>
-
#
-
# <%# app/views/users/_user.html.erb %>
-
# Name: <%= user.name %>
-
#
-
# <%# app/views/users/_administrator.html.erb %>
-
# <div id="administrator">
-
# Budget: $<%= user.budget %>
-
# <%= yield %>
-
# </div>
-
#
-
# <%# app/views/users/_editor.html.erb %>
-
# <div id="editor">
-
# Deadline: <%= user.deadline %>
-
# <%= yield %>
-
# </div>
-
#
-
# ...this will return:
-
#
-
# Here's the administrator:
-
# <div id="administrator">
-
# Budget: $<%= user.budget %>
-
# Name: <%= user.name %>
-
# </div>
-
#
-
# Here's the editor:
-
# <div id="editor">
-
# Deadline: <%= user.deadline %>
-
# Name: <%= user.name %>
-
# </div>
-
#
-
# If a collection is given, the layout will be rendered once for each item in
-
# the collection. For example, these two snippets have the same output:
-
#
-
# <%# app/views/users/_user.html.erb %>
-
# Name: <%= user.name %>
-
#
-
# <%# app/views/users/index.html.erb %>
-
# <%# This does not use layouts %>
-
# <ul>
-
# <% users.each do |user| -%>
-
# <li>
-
# <%= render partial: "user", locals: { user: user } %>
-
# </li>
-
# <% end -%>
-
# </ul>
-
#
-
# <%# app/views/users/_li_layout.html.erb %>
-
# <li>
-
# <%= yield %>
-
# </li>
-
#
-
# <%# app/views/users/index.html.erb %>
-
# <ul>
-
# <%= render partial: "user", layout: "li_layout", collection: users %>
-
# </ul>
-
#
-
# Given two users whose names are Alice and Bob, these snippets return:
-
#
-
# <ul>
-
# <li>
-
# Name: Alice
-
# </li>
-
# <li>
-
# Name: Bob
-
# </li>
-
# </ul>
-
#
-
# The current object being rendered, as well as the object_counter, will be
-
# available as local variables inside the layout template under the same names
-
# as available in the partial.
-
#
-
# You can also apply a layout to a block within any template:
-
#
-
# <%# app/views/users/_chief.html.erb %>
-
# <%= render(layout: "administrator", locals: { user: chief }) do %>
-
# Title: <%= chief.title %>
-
# <% end %>
-
#
-
# ...this will return:
-
#
-
# <div id="administrator">
-
# Budget: $<%= user.budget %>
-
# Title: <%= chief.name %>
-
# </div>
-
#
-
# As you can see, the <tt>:locals</tt> hash is shared between both the partial and its layout.
-
#
-
# If you pass arguments to "yield" then this will be passed to the block. One way to use this is to pass
-
# an array to layout and treat it as an enumerable.
-
#
-
# <%# app/views/users/_user.html.erb %>
-
# <div class="user">
-
# Budget: $<%= user.budget %>
-
# <%= yield user %>
-
# </div>
-
#
-
# <%# app/views/users/index.html.erb %>
-
# <%= render layout: @users do |user| %>
-
# Title: <%= user.title %>
-
# <% end %>
-
#
-
# This will render the layout for each user and yield to the block, passing the user, each time.
-
#
-
# You can also yield multiple times in one layout and use block arguments to differentiate the sections.
-
#
-
# <%# app/views/users/_user.html.erb %>
-
# <div class="user">
-
# <%= yield user, :header %>
-
# Budget: $<%= user.budget %>
-
# <%= yield user, :footer %>
-
# </div>
-
#
-
# <%# app/views/users/index.html.erb %>
-
# <%= render layout: @users do |user, section| %>
-
# <%- case section when :header -%>
-
# Title: <%= user.title %>
-
# <%- when :footer -%>
-
# Deadline: <%= user.deadline %>
-
# <%- end -%>
-
# <% end %>
-
1
class PartialRenderer < AbstractRenderer
-
1
include CollectionCaching
-
-
1
PREFIXED_PARTIAL_NAMES = Concurrent::Map.new do |h, k|
-
h[k] = Concurrent::Map.new
-
end
-
-
1
def initialize(*)
-
super
-
@context_prefix = @lookup_context.prefixes.first
-
end
-
-
1
def render(context, options, block)
-
setup(context, options, block)
-
@template = find_partial
-
-
@lookup_context.rendered_format ||= begin
-
if @template && @template.formats.present?
-
@template.formats.first
-
else
-
formats.first
-
end
-
end
-
-
if @collection
-
render_collection
-
else
-
render_partial
-
end
-
end
-
-
1
private
-
-
1
def render_collection
-
instrument(:collection, count: @collection.size) do |payload|
-
return nil if @collection.blank?
-
-
if @options.key?(:spacer_template)
-
spacer = find_template(@options[:spacer_template], @locals.keys).render(@view, @locals)
-
end
-
-
cache_collection_render(payload) do
-
@template ? collection_with_template : collection_without_template
-
end.join(spacer).html_safe
-
end
-
end
-
-
1
def render_partial
-
instrument(:partial) do |payload|
-
view, locals, block = @view, @locals, @block
-
object, as = @object, @variable
-
-
if !block && (layout = @options[:layout])
-
layout = find_template(layout.to_s, @template_keys)
-
end
-
-
object = locals[as] if object.nil? # Respect object when object is false
-
locals[as] = object if @has_object
-
-
content = @template.render(view, locals) do |*name|
-
view._layout_for(*name, &block)
-
end
-
-
content = layout.render(view, locals) { content } if layout
-
payload[:cache_hit] = view.view_renderer.cache_hits[@template.virtual_path]
-
content
-
end
-
end
-
-
# Sets up instance variables needed for rendering a partial. This method
-
# finds the options and details and extracts them. The method also contains
-
# logic that handles the type of object passed in as the partial.
-
#
-
# If +options[:partial]+ is a string, then the <tt>@path</tt> instance variable is
-
# set to that string. Otherwise, the +options[:partial]+ object must
-
# respond to +to_partial_path+ in order to setup the path.
-
1
def setup(context, options, block)
-
@view = context
-
@options = options
-
@block = block
-
-
@locals = options[:locals] || {}
-
@details = extract_details(options)
-
-
prepend_formats(options[:formats])
-
-
partial = options[:partial]
-
-
if String === partial
-
@has_object = options.key?(:object)
-
@object = options[:object]
-
@collection = collection_from_options
-
@path = partial
-
else
-
@has_object = true
-
@object = partial
-
@collection = collection_from_object || collection_from_options
-
-
if @collection
-
paths = @collection_data = @collection.map { |o| partial_path(o) }
-
@path = paths.uniq.one? ? paths.first : nil
-
else
-
@path = partial_path
-
end
-
end
-
-
if as = options[:as]
-
raise_invalid_option_as(as) unless /\A[a-z_]\w*\z/.match?(as.to_s)
-
as = as.to_sym
-
end
-
-
if @path
-
@variable, @variable_counter, @variable_iteration = retrieve_variable(@path, as)
-
@template_keys = retrieve_template_keys
-
else
-
paths.map! { |path| retrieve_variable(path, as).unshift(path) }
-
end
-
-
self
-
end
-
-
1
def collection_from_options
-
if @options.key?(:collection)
-
collection = @options[:collection]
-
collection ? collection.to_a : []
-
end
-
end
-
-
1
def collection_from_object
-
@object.to_ary if @object.respond_to?(:to_ary)
-
end
-
-
1
def find_partial
-
find_template(@path, @template_keys) if @path
-
end
-
-
1
def find_template(path, locals)
-
prefixes = path.include?(?/) ? [] : @lookup_context.prefixes
-
@lookup_context.find_template(path, prefixes, true, locals, @details)
-
end
-
-
1
def collection_with_template
-
view, locals, template = @view, @locals, @template
-
as, counter, iteration = @variable, @variable_counter, @variable_iteration
-
-
if layout = @options[:layout]
-
layout = find_template(layout, @template_keys)
-
end
-
-
partial_iteration = PartialIteration.new(@collection.size)
-
locals[iteration] = partial_iteration
-
-
@collection.map do |object|
-
locals[as] = object
-
locals[counter] = partial_iteration.index
-
-
content = template.render(view, locals)
-
content = layout.render(view, locals) { content } if layout
-
partial_iteration.iterate!
-
content
-
end
-
end
-
-
1
def collection_without_template
-
view, locals, collection_data = @view, @locals, @collection_data
-
cache = {}
-
keys = @locals.keys
-
-
partial_iteration = PartialIteration.new(@collection.size)
-
-
@collection.map do |object|
-
index = partial_iteration.index
-
path, as, counter, iteration = collection_data[index]
-
-
locals[as] = object
-
locals[counter] = index
-
locals[iteration] = partial_iteration
-
-
template = (cache[path] ||= find_template(path, keys + [as, counter, iteration]))
-
content = template.render(view, locals)
-
partial_iteration.iterate!
-
content
-
end
-
end
-
-
# Obtains the path to where the object's partial is located. If the object
-
# responds to +to_partial_path+, then +to_partial_path+ will be called and
-
# will provide the path. If the object does not respond to +to_partial_path+,
-
# then an +ArgumentError+ is raised.
-
#
-
# If +prefix_partial_path_with_controller_namespace+ is true, then this
-
# method will prefix the partial paths with a namespace.
-
1
def partial_path(object = @object)
-
object = object.to_model if object.respond_to?(:to_model)
-
-
path = if object.respond_to?(:to_partial_path)
-
object.to_partial_path
-
else
-
raise ArgumentError.new("'#{object.inspect}' is not an ActiveModel-compatible object. It must implement :to_partial_path.")
-
end
-
-
if @view.prefix_partial_path_with_controller_namespace
-
prefixed_partial_names[path] ||= merge_prefix_into_object_path(@context_prefix, path.dup)
-
else
-
path
-
end
-
end
-
-
1
def prefixed_partial_names
-
@prefixed_partial_names ||= PREFIXED_PARTIAL_NAMES[@context_prefix]
-
end
-
-
1
def merge_prefix_into_object_path(prefix, object_path)
-
if prefix.include?(?/) && object_path.include?(?/)
-
prefixes = []
-
prefix_array = File.dirname(prefix).split("/")
-
object_path_array = object_path.split("/")[0..-3] # skip model dir & partial
-
-
prefix_array.each_with_index do |dir, index|
-
break if dir == object_path_array[index]
-
prefixes << dir
-
end
-
-
(prefixes << object_path).join("/")
-
else
-
object_path
-
end
-
end
-
-
1
def retrieve_template_keys
-
keys = @locals.keys
-
keys << @variable if @has_object || @collection
-
if @collection
-
keys << @variable_counter
-
keys << @variable_iteration
-
end
-
keys
-
end
-
-
1
def retrieve_variable(path, as)
-
variable = as || begin
-
base = path[-1] == "/".freeze ? "".freeze : File.basename(path)
-
raise_invalid_identifier(path) unless base =~ /\A_?(.*?)(?:\.\w+)*\z/
-
$1.to_sym
-
end
-
if @collection
-
variable_counter = :"#{variable}_counter"
-
variable_iteration = :"#{variable}_iteration"
-
end
-
[variable, variable_counter, variable_iteration]
-
end
-
-
1
IDENTIFIER_ERROR_MESSAGE = "The partial name (%s) is not a valid Ruby identifier; " \
-
"make sure your partial name starts with underscore."
-
-
1
OPTION_AS_ERROR_MESSAGE = "The value (%s) of the option `as` is not a valid Ruby identifier; " \
-
"make sure it starts with lowercase letter, " \
-
"and is followed by any combination of letters, numbers and underscores."
-
-
1
def raise_invalid_identifier(path)
-
raise ArgumentError.new(IDENTIFIER_ERROR_MESSAGE % (path))
-
end
-
-
1
def raise_invalid_option_as(as)
-
raise ArgumentError.new(OPTION_AS_ERROR_MESSAGE % (as))
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActionView
-
1
module CollectionCaching # :nodoc:
-
1
extend ActiveSupport::Concern
-
-
1
included do
-
# Fallback cache store if Action View is used without Rails.
-
# Otherwise overridden in Railtie to use Rails.cache.
-
1
mattr_accessor :collection_cache, default: ActiveSupport::Cache::MemoryStore.new
-
end
-
-
1
private
-
1
def cache_collection_render(instrumentation_payload)
-
return yield unless @options[:cached]
-
-
keyed_collection = collection_by_cache_keys
-
cached_partials = collection_cache.read_multi(*keyed_collection.keys)
-
instrumentation_payload[:cache_hits] = cached_partials.size
-
-
@collection = keyed_collection.reject { |key, _| cached_partials.key?(key) }.values
-
rendered_partials = @collection.empty? ? [] : yield
-
-
index = 0
-
fetch_or_cache_partial(cached_partials, order_by: keyed_collection.each_key) do
-
rendered_partials[index].tap { index += 1 }
-
end
-
end
-
-
1
def callable_cache_key?
-
@options[:cached].respond_to?(:call)
-
end
-
-
1
def collection_by_cache_keys
-
seed = callable_cache_key? ? @options[:cached] : ->(i) { i }
-
-
@collection.each_with_object({}) do |item, hash|
-
hash[expanded_cache_key(seed.call(item))] = item
-
end
-
end
-
-
1
def expanded_cache_key(key)
-
key = @view.combined_fragment_cache_key(@view.cache_fragment_name(key, virtual_path: @template.virtual_path))
-
key.frozen? ? key.dup : key # #read_multi & #write may require mutability, Dalli 2.6.0.
-
end
-
-
1
def fetch_or_cache_partial(cached_partials, order_by:)
-
order_by.map do |cache_key|
-
cached_partials.fetch(cache_key) do
-
yield.tap do |rendered_partial|
-
collection_cache.write(cache_key, rendered_partial)
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "action_dispatch/routing/polymorphic_routes"
-
-
1
module ActionView
-
1
module RoutingUrlFor
-
# Returns the URL for the set of +options+ provided. This takes the
-
# same options as +url_for+ in Action Controller (see the
-
# documentation for <tt>ActionController::Base#url_for</tt>). Note that by default
-
# <tt>:only_path</tt> is <tt>true</tt> so you'll get the relative "/controller/action"
-
# instead of the fully qualified URL like "http://example.com/controller/action".
-
#
-
# ==== Options
-
# * <tt>:anchor</tt> - Specifies the anchor name to be appended to the path.
-
# * <tt>:only_path</tt> - If true, returns the relative URL (omitting the protocol, host name, and port) (<tt>true</tt> by default unless <tt>:host</tt> is specified).
-
# * <tt>:trailing_slash</tt> - If true, adds a trailing slash, as in "/archive/2005/". Note that this
-
# is currently not recommended since it breaks caching.
-
# * <tt>:host</tt> - Overrides the default (current) host if provided.
-
# * <tt>:protocol</tt> - Overrides the default (current) protocol if provided.
-
# * <tt>:user</tt> - Inline HTTP authentication (only plucked out if <tt>:password</tt> is also present).
-
# * <tt>:password</tt> - Inline HTTP authentication (only plucked out if <tt>:user</tt> is also present).
-
#
-
# ==== Relying on named routes
-
#
-
# Passing a record (like an Active Record) instead of a hash as the options parameter will
-
# trigger the named route for that record. The lookup will happen on the name of the class. So passing a
-
# Workshop object will attempt to use the +workshop_path+ route. If you have a nested route, such as
-
# +admin_workshop_path+ you'll have to call that explicitly (it's impossible for +url_for+ to guess that route).
-
#
-
# ==== Implicit Controller Namespacing
-
#
-
# Controllers passed in using the +:controller+ option will retain their namespace unless it is an absolute one.
-
#
-
# ==== Examples
-
# <%= url_for(action: 'index') %>
-
# # => /blogs/
-
#
-
# <%= url_for(action: 'find', controller: 'books') %>
-
# # => /books/find
-
#
-
# <%= url_for(action: 'login', controller: 'members', only_path: false, protocol: 'https') %>
-
# # => https://www.example.com/members/login/
-
#
-
# <%= url_for(action: 'play', anchor: 'player') %>
-
# # => /messages/play/#player
-
#
-
# <%= url_for(action: 'jump', anchor: 'tax&ship') %>
-
# # => /testing/jump/#tax&ship
-
#
-
# <%= url_for(Workshop.new) %>
-
# # relies on Workshop answering a persisted? call (and in this case returning false)
-
# # => /workshops
-
#
-
# <%= url_for(@workshop) %>
-
# # calls @workshop.to_param which by default returns the id
-
# # => /workshops/5
-
#
-
# # to_param can be re-defined in a model to provide different URL names:
-
# # => /workshops/1-workshop-name
-
#
-
# <%= url_for("http://www.example.com") %>
-
# # => http://www.example.com
-
#
-
# <%= url_for(:back) %>
-
# # if request.env["HTTP_REFERER"] is set to "http://www.example.com"
-
# # => http://www.example.com
-
#
-
# <%= url_for(:back) %>
-
# # if request.env["HTTP_REFERER"] is not set or is blank
-
# # => javascript:history.back()
-
#
-
# <%= url_for(action: 'index', controller: 'users') %>
-
# # Assuming an "admin" namespace
-
# # => /admin/users
-
#
-
# <%= url_for(action: 'index', controller: '/users') %>
-
# # Specify absolute path with beginning slash
-
# # => /users
-
1
def url_for(options = nil)
-
case options
-
when String
-
options
-
when nil
-
super(only_path: _generate_paths_by_default)
-
when Hash
-
options = options.symbolize_keys
-
unless options.key?(:only_path)
-
options[:only_path] = only_path?(options[:host])
-
end
-
-
super(options)
-
when ActionController::Parameters
-
unless options.key?(:only_path)
-
options[:only_path] = only_path?(options[:host])
-
end
-
-
super(options)
-
when :back
-
_back_url
-
when Array
-
components = options.dup
-
if _generate_paths_by_default
-
polymorphic_path(components, components.extract_options!)
-
else
-
polymorphic_url(components, components.extract_options!)
-
end
-
else
-
method = _generate_paths_by_default ? :path : :url
-
builder = ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder.send(method)
-
-
case options
-
when Symbol
-
builder.handle_string_call(self, options)
-
when Class
-
builder.handle_class_call(self, options)
-
else
-
builder.handle_model_call(self, options)
-
end
-
end
-
end
-
-
1
def url_options #:nodoc:
-
return super unless controller.respond_to?(:url_options)
-
controller.url_options
-
end
-
-
1
private
-
1
def _routes_context
-
controller
-
end
-
-
1
def optimize_routes_generation?
-
controller.respond_to?(:optimize_routes_generation?, true) ?
-
controller.optimize_routes_generation? : super
-
end
-
-
1
def _generate_paths_by_default
-
true
-
end
-
-
1
def only_path?(host)
-
_generate_paths_by_default unless host
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_support/core_ext/hash"
-
-
1
module ActiveJob
-
# Raised when an exception is raised during job arguments deserialization.
-
#
-
# Wraps the original exception raised as +cause+.
-
1
class DeserializationError < StandardError
-
1
def initialize #:nodoc:
-
super("Error while trying to deserialize arguments: #{$!.message}")
-
set_backtrace $!.backtrace
-
end
-
end
-
-
# Raised when an unsupported argument type is set as a job argument. We
-
# currently support NilClass, Integer, Fixnum, Float, String, TrueClass, FalseClass,
-
# Bignum, BigDecimal, and objects that can be represented as GlobalIDs (ex: Active Record).
-
# Raised if you set the key for a Hash something else than a string or
-
# a symbol. Also raised when trying to serialize an object which can't be
-
# identified with a Global ID - such as an unpersisted Active Record model.
-
1
class SerializationError < ArgumentError; end
-
-
1
module Arguments
-
1
extend self
-
# :nodoc:
-
1
TYPE_WHITELIST = [ NilClass, String, Integer, Float, BigDecimal, TrueClass, FalseClass ]
-
1
TYPE_WHITELIST.push(Fixnum, Bignum) unless 1.class == Integer
-
-
# Serializes a set of arguments. Whitelisted types are returned
-
# as-is. Arrays/Hashes are serialized element by element.
-
# All other types are serialized using GlobalID.
-
1
def serialize(arguments)
-
arguments.map { |argument| serialize_argument(argument) }
-
end
-
-
# Deserializes a set of arguments. Whitelisted types are returned
-
# as-is. Arrays/Hashes are deserialized element by element.
-
# All other types are deserialized using GlobalID.
-
1
def deserialize(arguments)
-
arguments.map { |argument| deserialize_argument(argument) }
-
rescue
-
raise DeserializationError
-
end
-
-
1
private
-
# :nodoc:
-
1
GLOBALID_KEY = "_aj_globalid".freeze
-
# :nodoc:
-
1
SYMBOL_KEYS_KEY = "_aj_symbol_keys".freeze
-
# :nodoc:
-
1
WITH_INDIFFERENT_ACCESS_KEY = "_aj_hash_with_indifferent_access".freeze
-
1
private_constant :GLOBALID_KEY, :SYMBOL_KEYS_KEY, :WITH_INDIFFERENT_ACCESS_KEY
-
-
1
def serialize_argument(argument)
-
case argument
-
when *TYPE_WHITELIST
-
argument
-
when GlobalID::Identification
-
convert_to_global_id_hash(argument)
-
when Array
-
argument.map { |arg| serialize_argument(arg) }
-
when ActiveSupport::HashWithIndifferentAccess
-
serialize_indifferent_hash(argument)
-
when Hash
-
symbol_keys = argument.each_key.grep(Symbol).map(&:to_s)
-
result = serialize_hash(argument)
-
result[SYMBOL_KEYS_KEY] = symbol_keys
-
result
-
when -> (arg) { arg.respond_to?(:permitted?) }
-
serialize_indifferent_hash(argument.to_h)
-
else
-
raise SerializationError.new("Unsupported argument type: #{argument.class.name}")
-
end
-
end
-
-
1
def deserialize_argument(argument)
-
case argument
-
when String
-
argument
-
when *TYPE_WHITELIST
-
argument
-
when Array
-
argument.map { |arg| deserialize_argument(arg) }
-
when Hash
-
if serialized_global_id?(argument)
-
deserialize_global_id argument
-
else
-
deserialize_hash(argument)
-
end
-
else
-
raise ArgumentError, "Can only deserialize primitive arguments: #{argument.inspect}"
-
end
-
end
-
-
1
def serialized_global_id?(hash)
-
hash.size == 1 && hash.include?(GLOBALID_KEY)
-
end
-
-
1
def deserialize_global_id(hash)
-
GlobalID::Locator.locate hash[GLOBALID_KEY]
-
end
-
-
1
def serialize_hash(argument)
-
argument.each_with_object({}) do |(key, value), hash|
-
hash[serialize_hash_key(key)] = serialize_argument(value)
-
end
-
end
-
-
1
def deserialize_hash(serialized_hash)
-
result = serialized_hash.transform_values { |v| deserialize_argument(v) }
-
if result.delete(WITH_INDIFFERENT_ACCESS_KEY)
-
result = result.with_indifferent_access
-
elsif symbol_keys = result.delete(SYMBOL_KEYS_KEY)
-
result = transform_symbol_keys(result, symbol_keys)
-
end
-
result
-
end
-
-
# :nodoc:
-
1
RESERVED_KEYS = [
-
GLOBALID_KEY, GLOBALID_KEY.to_sym,
-
SYMBOL_KEYS_KEY, SYMBOL_KEYS_KEY.to_sym,
-
WITH_INDIFFERENT_ACCESS_KEY, WITH_INDIFFERENT_ACCESS_KEY.to_sym,
-
]
-
1
private_constant :RESERVED_KEYS
-
-
1
def serialize_hash_key(key)
-
case key
-
when *RESERVED_KEYS
-
raise SerializationError.new("Can't serialize a Hash with reserved key #{key.inspect}")
-
when String, Symbol
-
key.to_s
-
else
-
raise SerializationError.new("Only string and symbol hash keys may be serialized as job arguments, but #{key.inspect} is a #{key.class}")
-
end
-
end
-
-
1
def serialize_indifferent_hash(indifferent_hash)
-
result = serialize_hash(indifferent_hash)
-
result[WITH_INDIFFERENT_ACCESS_KEY] = serialize_argument(true)
-
result
-
end
-
-
1
def transform_symbol_keys(hash, symbol_keys)
-
# NOTE: HashWithIndifferentAccess#transform_keys always
-
# returns stringified keys with indifferent access
-
# so we call #to_h here to ensure keys are symbolized.
-
hash.to_h.transform_keys do |key|
-
if symbol_keys.include?(key)
-
key.to_sym
-
else
-
key
-
end
-
end
-
end
-
-
1
def convert_to_global_id_hash(argument)
-
{ GLOBALID_KEY => argument.to_global_id.to_s }
-
rescue URI::GID::MissingModelIdError
-
raise SerializationError, "Unable to serialize #{argument.class} " \
-
"without an id. (Maybe you forgot to call save?)"
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_job/core"
-
1
require "active_job/queue_adapter"
-
1
require "active_job/queue_name"
-
1
require "active_job/queue_priority"
-
1
require "active_job/enqueuing"
-
1
require "active_job/execution"
-
1
require "active_job/callbacks"
-
1
require "active_job/exceptions"
-
1
require "active_job/logging"
-
1
require "active_job/translation"
-
-
1
module ActiveJob #:nodoc:
-
# = Active Job
-
#
-
# Active Job objects can be configured to work with different backend
-
# queuing frameworks. To specify a queue adapter to use:
-
#
-
# ActiveJob::Base.queue_adapter = :inline
-
#
-
# A list of supported adapters can be found in QueueAdapters.
-
#
-
# Active Job objects can be defined by creating a class that inherits
-
# from the ActiveJob::Base class. The only necessary method to
-
# implement is the "perform" method.
-
#
-
# To define an Active Job object:
-
#
-
# class ProcessPhotoJob < ActiveJob::Base
-
# def perform(photo)
-
# photo.watermark!('Rails')
-
# photo.rotate!(90.degrees)
-
# photo.resize_to_fit!(300, 300)
-
# photo.upload!
-
# end
-
# end
-
#
-
# Records that are passed in are serialized/deserialized using Global
-
# ID. More information can be found in Arguments.
-
#
-
# To enqueue a job to be performed as soon as the queueing system is free:
-
#
-
# ProcessPhotoJob.perform_later(photo)
-
#
-
# To enqueue a job to be processed at some point in the future:
-
#
-
# ProcessPhotoJob.set(wait_until: Date.tomorrow.noon).perform_later(photo)
-
#
-
# More information can be found in ActiveJob::Core::ClassMethods#set
-
#
-
# A job can also be processed immediately without sending to the queue:
-
#
-
# ProcessPhotoJob.perform_now(photo)
-
#
-
# == Exceptions
-
#
-
# * DeserializationError - Error class for deserialization errors.
-
# * SerializationError - Error class for serialization errors.
-
1
class Base
-
1
include Core
-
1
include QueueAdapter
-
1
include QueueName
-
1
include QueuePriority
-
1
include Enqueuing
-
1
include Execution
-
1
include Callbacks
-
1
include Exceptions
-
1
include Logging
-
1
include Translation
-
-
1
ActiveSupport.run_load_hooks(:active_job, self)
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_support/callbacks"
-
-
1
module ActiveJob
-
# = Active Job Callbacks
-
#
-
# Active Job provides hooks during the life cycle of a job. Callbacks allow you
-
# to trigger logic during this cycle. Available callbacks are:
-
#
-
# * <tt>before_enqueue</tt>
-
# * <tt>around_enqueue</tt>
-
# * <tt>after_enqueue</tt>
-
# * <tt>before_perform</tt>
-
# * <tt>around_perform</tt>
-
# * <tt>after_perform</tt>
-
#
-
# NOTE: Calling the same callback multiple times will overwrite previous callback definitions.
-
#
-
1
module Callbacks
-
1
extend ActiveSupport::Concern
-
1
include ActiveSupport::Callbacks
-
-
1
class << self
-
1
include ActiveSupport::Callbacks
-
1
define_callbacks :execute
-
end
-
-
1
included do
-
1
define_callbacks :perform
-
1
define_callbacks :enqueue
-
end
-
-
# These methods will be included into any Active Job object, adding
-
# callbacks for +perform+ and +enqueue+ methods.
-
1
module ClassMethods
-
# Defines a callback that will get called right before the
-
# job's perform method is executed.
-
#
-
# class VideoProcessJob < ActiveJob::Base
-
# queue_as :default
-
#
-
# before_perform do |job|
-
# UserMailer.notify_video_started_processing(job.arguments.first)
-
# end
-
#
-
# def perform(video_id)
-
# Video.find(video_id).process
-
# end
-
# end
-
#
-
1
def before_perform(*filters, &blk)
-
set_callback(:perform, :before, *filters, &blk)
-
end
-
-
# Defines a callback that will get called right after the
-
# job's perform method has finished.
-
#
-
# class VideoProcessJob < ActiveJob::Base
-
# queue_as :default
-
#
-
# after_perform do |job|
-
# UserMailer.notify_video_processed(job.arguments.first)
-
# end
-
#
-
# def perform(video_id)
-
# Video.find(video_id).process
-
# end
-
# end
-
#
-
1
def after_perform(*filters, &blk)
-
set_callback(:perform, :after, *filters, &blk)
-
end
-
-
# Defines a callback that will get called around the job's perform method.
-
#
-
# class VideoProcessJob < ActiveJob::Base
-
# queue_as :default
-
#
-
# around_perform do |job, block|
-
# UserMailer.notify_video_started_processing(job.arguments.first)
-
# block.call
-
# UserMailer.notify_video_processed(job.arguments.first)
-
# end
-
#
-
# def perform(video_id)
-
# Video.find(video_id).process
-
# end
-
# end
-
#
-
1
def around_perform(*filters, &blk)
-
2
set_callback(:perform, :around, *filters, &blk)
-
end
-
-
# Defines a callback that will get called right before the
-
# job is enqueued.
-
#
-
# class VideoProcessJob < ActiveJob::Base
-
# queue_as :default
-
#
-
# before_enqueue do |job|
-
# $statsd.increment "enqueue-video-job.try"
-
# end
-
#
-
# def perform(video_id)
-
# Video.find(video_id).process
-
# end
-
# end
-
#
-
1
def before_enqueue(*filters, &blk)
-
set_callback(:enqueue, :before, *filters, &blk)
-
end
-
-
# Defines a callback that will get called right after the
-
# job is enqueued.
-
#
-
# class VideoProcessJob < ActiveJob::Base
-
# queue_as :default
-
#
-
# after_enqueue do |job|
-
# $statsd.increment "enqueue-video-job.success"
-
# end
-
#
-
# def perform(video_id)
-
# Video.find(video_id).process
-
# end
-
# end
-
#
-
1
def after_enqueue(*filters, &blk)
-
1
set_callback(:enqueue, :after, *filters, &blk)
-
end
-
-
# Defines a callback that will get called around the enqueueing
-
# of the job.
-
#
-
# class VideoProcessJob < ActiveJob::Base
-
# queue_as :default
-
#
-
# around_enqueue do |job, block|
-
# $statsd.time "video-job.process" do
-
# block.call
-
# end
-
# end
-
#
-
# def perform(video_id)
-
# Video.find(video_id).process
-
# end
-
# end
-
#
-
1
def around_enqueue(*filters, &blk)
-
1
set_callback(:enqueue, :around, *filters, &blk)
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActiveJob
-
# Provides general behavior that will be included into every Active Job
-
# object that inherits from ActiveJob::Base.
-
1
module Core
-
1
extend ActiveSupport::Concern
-
-
1
included do
-
# Job arguments
-
1
attr_accessor :arguments
-
1
attr_writer :serialized_arguments
-
-
# Timestamp when the job should be performed
-
1
attr_accessor :scheduled_at
-
-
# Job Identifier
-
1
attr_accessor :job_id
-
-
# Queue in which the job will reside.
-
1
attr_writer :queue_name
-
-
# Priority that the job will have (lower is more priority).
-
1
attr_writer :priority
-
-
# ID optionally provided by adapter
-
1
attr_accessor :provider_job_id
-
-
# Number of times this job has been executed (which increments on every retry, like after an exception).
-
1
attr_accessor :executions
-
-
# I18n.locale to be used during the job.
-
1
attr_accessor :locale
-
end
-
-
# These methods will be included into any Active Job object, adding
-
# helpers for de/serialization and creation of job instances.
-
1
module ClassMethods
-
# Creates a new job instance from a hash created with +serialize+
-
1
def deserialize(job_data)
-
job = job_data["job_class"].constantize.new
-
job.deserialize(job_data)
-
job
-
end
-
-
# Creates a job preconfigured with the given options. You can call
-
# perform_later with the job arguments to enqueue the job with the
-
# preconfigured options
-
#
-
# ==== Options
-
# * <tt>:wait</tt> - Enqueues the job with the specified delay
-
# * <tt>:wait_until</tt> - Enqueues the job at the time specified
-
# * <tt>:queue</tt> - Enqueues the job on the specified queue
-
# * <tt>:priority</tt> - Enqueues the job with the specified priority
-
#
-
# ==== Examples
-
#
-
# VideoJob.set(queue: :some_queue).perform_later(Video.last)
-
# VideoJob.set(wait: 5.minutes).perform_later(Video.last)
-
# VideoJob.set(wait_until: Time.now.tomorrow).perform_later(Video.last)
-
# VideoJob.set(queue: :some_queue, wait: 5.minutes).perform_later(Video.last)
-
# VideoJob.set(queue: :some_queue, wait_until: Time.now.tomorrow).perform_later(Video.last)
-
# VideoJob.set(queue: :some_queue, wait: 5.minutes, priority: 10).perform_later(Video.last)
-
1
def set(options = {})
-
ConfiguredJob.new(self, options)
-
end
-
end
-
-
# Creates a new job instance. Takes the arguments that will be
-
# passed to the perform method.
-
1
def initialize(*arguments)
-
@arguments = arguments
-
@job_id = SecureRandom.uuid
-
@queue_name = self.class.queue_name
-
@priority = self.class.priority
-
@executions = 0
-
end
-
-
# Returns a hash with the job data that can safely be passed to the
-
# queueing adapter.
-
1
def serialize
-
{
-
"job_class" => self.class.name,
-
"job_id" => job_id,
-
"provider_job_id" => provider_job_id,
-
"queue_name" => queue_name,
-
"priority" => priority,
-
"arguments" => serialize_arguments_if_needed(arguments),
-
"executions" => executions,
-
"locale" => I18n.locale.to_s
-
}
-
end
-
-
# Attaches the stored job data to the current instance. Receives a hash
-
# returned from +serialize+
-
#
-
# ==== Examples
-
#
-
# class DeliverWebhookJob < ActiveJob::Base
-
# attr_writer :attempt_number
-
#
-
# def attempt_number
-
# @attempt_number ||= 0
-
# end
-
#
-
# def serialize
-
# super.merge('attempt_number' => attempt_number + 1)
-
# end
-
#
-
# def deserialize(job_data)
-
# super
-
# self.attempt_number = job_data['attempt_number']
-
# end
-
#
-
# rescue_from(Timeout::Error) do |exception|
-
# raise exception if attempt_number > 5
-
# retry_job(wait: 10)
-
# end
-
# end
-
1
def deserialize(job_data)
-
self.job_id = job_data["job_id"]
-
self.provider_job_id = job_data["provider_job_id"]
-
self.queue_name = job_data["queue_name"]
-
self.priority = job_data["priority"]
-
self.serialized_arguments = job_data["arguments"]
-
self.executions = job_data["executions"]
-
self.locale = job_data["locale"] || I18n.locale.to_s
-
end
-
-
1
private
-
1
def serialize_arguments_if_needed(arguments)
-
if arguments_serialized?
-
@serialized_arguments
-
else
-
serialize_arguments(arguments)
-
end
-
end
-
-
1
def deserialize_arguments_if_needed
-
if arguments_serialized?
-
@arguments = deserialize_arguments(@serialized_arguments)
-
@serialized_arguments = nil
-
end
-
end
-
-
1
def serialize_arguments(arguments)
-
Arguments.serialize(arguments)
-
end
-
-
1
def deserialize_arguments(serialized_args)
-
Arguments.deserialize(serialized_args)
-
end
-
-
1
def arguments_serialized?
-
defined?(@serialized_arguments) && @serialized_arguments
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_job/arguments"
-
-
1
module ActiveJob
-
# Provides behavior for enqueuing jobs.
-
1
module Enqueuing
-
1
extend ActiveSupport::Concern
-
-
# Includes the +perform_later+ method for job initialization.
-
1
module ClassMethods
-
# Push a job onto the queue. The arguments must be legal JSON types
-
# (+string+, +int+, +float+, +nil+, +true+, +false+, +hash+ or +array+) or
-
# GlobalID::Identification instances. Arbitrary Ruby objects
-
# are not supported.
-
#
-
# Returns an instance of the job class queued with arguments available in
-
# Job#arguments.
-
1
def perform_later(*args)
-
job_or_instantiate(*args).enqueue
-
end
-
-
1
private
-
1
def job_or_instantiate(*args) # :doc:
-
args.first.is_a?(self) ? args.first : new(*args)
-
end
-
end
-
-
# Enqueues the job to be performed by the queue adapter.
-
#
-
# ==== Options
-
# * <tt>:wait</tt> - Enqueues the job with the specified delay
-
# * <tt>:wait_until</tt> - Enqueues the job at the time specified
-
# * <tt>:queue</tt> - Enqueues the job on the specified queue
-
# * <tt>:priority</tt> - Enqueues the job with the specified priority
-
#
-
# ==== Examples
-
#
-
# my_job_instance.enqueue
-
# my_job_instance.enqueue wait: 5.minutes
-
# my_job_instance.enqueue queue: :important
-
# my_job_instance.enqueue wait_until: Date.tomorrow.midnight
-
# my_job_instance.enqueue priority: 10
-
1
def enqueue(options = {})
-
self.scheduled_at = options[:wait].seconds.from_now.to_f if options[:wait]
-
self.scheduled_at = options[:wait_until].to_f if options[:wait_until]
-
self.queue_name = self.class.queue_name_from_part(options[:queue]) if options[:queue]
-
self.priority = options[:priority].to_i if options[:priority]
-
run_callbacks :enqueue do
-
if scheduled_at
-
self.class.queue_adapter.enqueue_at self, scheduled_at
-
else
-
self.class.queue_adapter.enqueue self
-
end
-
end
-
self
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_support/core_ext/numeric/time"
-
-
1
module ActiveJob
-
# Provides behavior for retrying and discarding jobs on exceptions.
-
1
module Exceptions
-
1
extend ActiveSupport::Concern
-
-
1
module ClassMethods
-
# Catch the exception and reschedule job for re-execution after so many seconds, for a specific number of attempts.
-
# If the exception keeps getting raised beyond the specified number of attempts, the exception is allowed to
-
# bubble up to the underlying queuing system, which may have its own retry mechanism or place it in a
-
# holding queue for inspection.
-
#
-
# You can also pass a block that'll be invoked if the retry attempts fail for custom logic rather than letting
-
# the exception bubble up. This block is yielded with the job instance as the first and the error instance as the second parameter.
-
#
-
# ==== Options
-
# * <tt>:wait</tt> - Re-enqueues the job with a delay specified either in seconds (default: 3 seconds),
-
# as a computing proc that the number of executions so far as an argument, or as a symbol reference of
-
# <tt>:exponentially_longer</tt>, which applies the wait algorithm of <tt>(executions ** 4) + 2</tt>
-
# (first wait 3s, then 18s, then 83s, etc)
-
# * <tt>:attempts</tt> - Re-enqueues the job the specified number of times (default: 5 attempts)
-
# * <tt>:queue</tt> - Re-enqueues the job on a different queue
-
# * <tt>:priority</tt> - Re-enqueues the job with a different priority
-
#
-
# ==== Examples
-
#
-
# class RemoteServiceJob < ActiveJob::Base
-
# retry_on CustomAppException # defaults to 3s wait, 5 attempts
-
# retry_on AnotherCustomAppException, wait: ->(executions) { executions * 2 }
-
# retry_on(YetAnotherCustomAppException) do |job, error|
-
# ExceptionNotifier.caught(error)
-
# end
-
# retry_on ActiveRecord::Deadlocked, wait: 5.seconds, attempts: 3
-
# retry_on Net::OpenTimeout, wait: :exponentially_longer, attempts: 10
-
#
-
# def perform(*args)
-
# # Might raise CustomAppException, AnotherCustomAppException, or YetAnotherCustomAppException for something domain specific
-
# # Might raise ActiveRecord::Deadlocked when a local db deadlock is detected
-
# # Might raise Net::OpenTimeout when the remote service is down
-
# end
-
# end
-
1
def retry_on(exception, wait: 3.seconds, attempts: 5, queue: nil, priority: nil)
-
rescue_from exception do |error|
-
if executions < attempts
-
logger.error "Retrying #{self.class} in #{wait} seconds, due to a #{exception}. The original exception was #{error.cause.inspect}."
-
retry_job wait: determine_delay(wait), queue: queue, priority: priority
-
else
-
if block_given?
-
yield self, error
-
else
-
logger.error "Stopped retrying #{self.class} due to a #{exception}, which reoccurred on #{executions} attempts. The original exception was #{error.cause.inspect}."
-
raise error
-
end
-
end
-
end
-
end
-
-
# Discard the job with no attempts to retry, if the exception is raised. This is useful when the subject of the job,
-
# like an Active Record, is no longer available, and the job is thus no longer relevant.
-
#
-
# You can also pass a block that'll be invoked. This block is yielded with the job instance as the first and the error instance as the second parameter.
-
#
-
# ==== Example
-
#
-
# class SearchIndexingJob < ActiveJob::Base
-
# discard_on ActiveJob::DeserializationError
-
# discard_on(CustomAppException) do |job, error|
-
# ExceptionNotifier.caught(error)
-
# end
-
#
-
# def perform(record)
-
# # Will raise ActiveJob::DeserializationError if the record can't be deserialized
-
# # Might raise CustomAppException for something domain specific
-
# end
-
# end
-
1
def discard_on(exception)
-
rescue_from exception do |error|
-
if block_given?
-
yield self, error
-
else
-
logger.error "Discarded #{self.class} due to a #{exception}. The original exception was #{error.cause.inspect}."
-
end
-
end
-
end
-
end
-
-
# Reschedules the job to be re-executed. This is useful in combination
-
# with the +rescue_from+ option. When you rescue an exception from your job
-
# you can ask Active Job to retry performing your job.
-
#
-
# ==== Options
-
# * <tt>:wait</tt> - Enqueues the job with the specified delay in seconds
-
# * <tt>:wait_until</tt> - Enqueues the job at the time specified
-
# * <tt>:queue</tt> - Enqueues the job on the specified queue
-
# * <tt>:priority</tt> - Enqueues the job with the specified priority
-
#
-
# ==== Examples
-
#
-
# class SiteScraperJob < ActiveJob::Base
-
# rescue_from(ErrorLoadingSite) do
-
# retry_job queue: :low_priority
-
# end
-
#
-
# def perform(*args)
-
# # raise ErrorLoadingSite if cannot scrape
-
# end
-
# end
-
1
def retry_job(options = {})
-
enqueue options
-
end
-
-
1
private
-
1
def determine_delay(seconds_or_duration_or_algorithm)
-
case seconds_or_duration_or_algorithm
-
when :exponentially_longer
-
(executions**4) + 2
-
when ActiveSupport::Duration
-
duration = seconds_or_duration_or_algorithm
-
duration.to_i
-
when Integer
-
seconds = seconds_or_duration_or_algorithm
-
seconds
-
when Proc
-
algorithm = seconds_or_duration_or_algorithm
-
algorithm.call(executions)
-
else
-
raise "Couldn't determine a delay based on #{seconds_or_duration_or_algorithm.inspect}"
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_support/rescuable"
-
1
require "active_job/arguments"
-
-
1
module ActiveJob
-
1
module Execution
-
1
extend ActiveSupport::Concern
-
1
include ActiveSupport::Rescuable
-
-
# Includes methods for executing and performing jobs instantly.
-
1
module ClassMethods
-
# Performs the job immediately.
-
#
-
# MyJob.perform_now("mike")
-
#
-
1
def perform_now(*args)
-
job_or_instantiate(*args).perform_now
-
end
-
-
1
def execute(job_data) #:nodoc:
-
ActiveJob::Callbacks.run_callbacks(:execute) do
-
job = deserialize(job_data)
-
job.perform_now
-
end
-
end
-
end
-
-
# Performs the job immediately. The job is not sent to the queueing adapter
-
# but directly executed by blocking the execution of others until it's finished.
-
#
-
# MyJob.new(*args).perform_now
-
1
def perform_now
-
# Guard against jobs that were persisted before we started counting executions by zeroing out nil counters
-
self.executions = (executions || 0) + 1
-
-
deserialize_arguments_if_needed
-
run_callbacks :perform do
-
perform(*arguments)
-
end
-
rescue => exception
-
rescue_with_handler(exception) || raise
-
end
-
-
1
def perform(*)
-
fail NotImplementedError
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_support/core_ext/hash/transform_values"
-
1
require "active_support/core_ext/string/filters"
-
1
require "active_support/tagged_logging"
-
1
require "active_support/logger"
-
-
1
module ActiveJob
-
1
module Logging #:nodoc:
-
1
extend ActiveSupport::Concern
-
-
1
included do
-
1
cattr_accessor :logger, default: ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(STDOUT))
-
-
1
around_enqueue do |_, block, _|
-
tag_logger do
-
block.call
-
end
-
end
-
-
1
around_perform do |job, block, _|
-
tag_logger(job.class.name, job.job_id) do
-
payload = { adapter: job.class.queue_adapter, job: job }
-
ActiveSupport::Notifications.instrument("perform_start.active_job", payload.dup)
-
ActiveSupport::Notifications.instrument("perform.active_job", payload) do
-
block.call
-
end
-
end
-
end
-
-
1
after_enqueue do |job|
-
if job.scheduled_at
-
ActiveSupport::Notifications.instrument "enqueue_at.active_job",
-
adapter: job.class.queue_adapter, job: job
-
else
-
ActiveSupport::Notifications.instrument "enqueue.active_job",
-
adapter: job.class.queue_adapter, job: job
-
end
-
end
-
end
-
-
1
private
-
1
def tag_logger(*tags)
-
if logger.respond_to?(:tagged)
-
tags.unshift "ActiveJob" unless logger_tagged_by_active_job?
-
logger.tagged(*tags) { yield }
-
else
-
yield
-
end
-
end
-
-
1
def logger_tagged_by_active_job?
-
logger.formatter.current_tags.include?("ActiveJob")
-
end
-
-
1
class LogSubscriber < ActiveSupport::LogSubscriber #:nodoc:
-
1
def enqueue(event)
-
info do
-
job = event.payload[:job]
-
"Enqueued #{job.class.name} (Job ID: #{job.job_id}) to #{queue_name(event)}" + args_info(job)
-
end
-
end
-
-
1
def enqueue_at(event)
-
info do
-
job = event.payload[:job]
-
"Enqueued #{job.class.name} (Job ID: #{job.job_id}) to #{queue_name(event)} at #{scheduled_at(event)}" + args_info(job)
-
end
-
end
-
-
1
def perform_start(event)
-
info do
-
job = event.payload[:job]
-
"Performing #{job.class.name} (Job ID: #{job.job_id}) from #{queue_name(event)}" + args_info(job)
-
end
-
end
-
-
1
def perform(event)
-
job = event.payload[:job]
-
ex = event.payload[:exception_object]
-
if ex
-
error do
-
"Error performing #{job.class.name} (Job ID: #{job.job_id}) from #{queue_name(event)} in #{event.duration.round(2)}ms: #{ex.class} (#{ex.message}):\n" + Array(ex.backtrace).join("\n")
-
end
-
else
-
info do
-
"Performed #{job.class.name} (Job ID: #{job.job_id}) from #{queue_name(event)} in #{event.duration.round(2)}ms"
-
end
-
end
-
end
-
-
1
private
-
1
def queue_name(event)
-
event.payload[:adapter].class.name.demodulize.remove("Adapter") + "(#{event.payload[:job].queue_name})"
-
end
-
-
1
def args_info(job)
-
if job.arguments.any?
-
" with arguments: " +
-
job.arguments.map { |arg| format(arg).inspect }.join(", ")
-
else
-
""
-
end
-
end
-
-
1
def format(arg)
-
case arg
-
when Hash
-
arg.transform_values { |value| format(value) }
-
when Array
-
arg.map { |value| format(value) }
-
when GlobalID::Identification
-
arg.to_global_id rescue arg
-
else
-
arg
-
end
-
end
-
-
1
def scheduled_at(event)
-
Time.at(event.payload[:job].scheduled_at).utc
-
end
-
-
1
def logger
-
ActiveJob::Base.logger
-
end
-
end
-
end
-
end
-
-
1
ActiveJob::Logging::LogSubscriber.attach_to :active_job
-
# frozen_string_literal: true
-
-
1
require "active_support/core_ext/string/inflections"
-
-
1
module ActiveJob
-
# The <tt>ActiveJob::QueueAdapter</tt> module is used to load the
-
# correct adapter. The default queue adapter is the +:async+ queue.
-
1
module QueueAdapter #:nodoc:
-
1
extend ActiveSupport::Concern
-
-
1
included do
-
1
class_attribute :_queue_adapter_name, instance_accessor: false, instance_predicate: false
-
1
class_attribute :_queue_adapter, instance_accessor: false, instance_predicate: false
-
1
self.queue_adapter = :async
-
end
-
-
# Includes the setter method for changing the active queue adapter.
-
1
module ClassMethods
-
# Returns the backend queue provider. The default queue adapter
-
# is the +:async+ queue. See QueueAdapters for more information.
-
1
def queue_adapter
-
_queue_adapter
-
end
-
-
1
def queue_adapter_name
-
_queue_adapter_name
-
end
-
-
# Specify the backend queue provider. The default queue adapter
-
# is the +:async+ queue. See QueueAdapters for more
-
# information.
-
1
def queue_adapter=(name_or_adapter)
-
2
case name_or_adapter
-
when Symbol, String
-
2
queue_adapter = ActiveJob::QueueAdapters.lookup(name_or_adapter).new
-
2
assign_adapter(name_or_adapter.to_s, queue_adapter)
-
else
-
if queue_adapter?(name_or_adapter)
-
adapter_name = "#{name_or_adapter.class.name.demodulize.remove('Adapter').underscore}"
-
assign_adapter(adapter_name, name_or_adapter)
-
else
-
raise ArgumentError
-
end
-
end
-
end
-
-
1
private
-
1
def assign_adapter(adapter_name, queue_adapter)
-
2
self._queue_adapter_name = adapter_name
-
2
self._queue_adapter = queue_adapter
-
end
-
-
1
QUEUE_ADAPTER_METHODS = [:enqueue, :enqueue_at].freeze
-
-
1
def queue_adapter?(object)
-
QUEUE_ADAPTER_METHODS.all? { |meth| object.respond_to?(meth) }
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActiveJob
-
# == Active Job adapters
-
#
-
# Active Job has adapters for the following queueing backends:
-
#
-
# * {Backburner}[https://github.com/nesquena/backburner]
-
# * {Delayed Job}[https://github.com/collectiveidea/delayed_job]
-
# * {Qu}[https://github.com/bkeepers/qu]
-
# * {Que}[https://github.com/chanks/que]
-
# * {queue_classic}[https://github.com/QueueClassic/queue_classic]
-
# * {Resque}[https://github.com/resque/resque]
-
# * {Sidekiq}[http://sidekiq.org]
-
# * {Sneakers}[https://github.com/jondot/sneakers]
-
# * {Sucker Punch}[https://github.com/brandonhilkert/sucker_punch]
-
# * {Active Job Async Job}[http://api.rubyonrails.org/classes/ActiveJob/QueueAdapters/AsyncAdapter.html]
-
# * {Active Job Inline}[http://api.rubyonrails.org/classes/ActiveJob/QueueAdapters/InlineAdapter.html]
-
#
-
# === Backends Features
-
#
-
# | | Async | Queues | Delayed | Priorities | Timeout | Retries |
-
# |-------------------|-------|--------|------------|------------|---------|---------|
-
# | Backburner | Yes | Yes | Yes | Yes | Job | Global |
-
# | Delayed Job | Yes | Yes | Yes | Job | Global | Global |
-
# | Qu | Yes | Yes | No | No | No | Global |
-
# | Que | Yes | Yes | Yes | Job | No | Job |
-
# | queue_classic | Yes | Yes | Yes* | No | No | No |
-
# | Resque | Yes | Yes | Yes (Gem) | Queue | Global | Yes |
-
# | Sidekiq | Yes | Yes | Yes | Queue | No | Job |
-
# | Sneakers | Yes | Yes | No | Queue | Queue | No |
-
# | Sucker Punch | Yes | Yes | Yes | No | No | No |
-
# | Active Job Async | Yes | Yes | Yes | No | No | No |
-
# | Active Job Inline | No | Yes | N/A | N/A | N/A | N/A |
-
#
-
# ==== Async
-
#
-
# Yes: The Queue Adapter has the ability to run the job in a non-blocking manner.
-
# It either runs on a separate or forked process, or on a different thread.
-
#
-
# No: The job is run in the same process.
-
#
-
# ==== Queues
-
#
-
# Yes: Jobs may set which queue they are run in with queue_as or by using the set
-
# method.
-
#
-
# ==== Delayed
-
#
-
# Yes: The adapter will run the job in the future through perform_later.
-
#
-
# (Gem): An additional gem is required to use perform_later with this adapter.
-
#
-
# No: The adapter will run jobs at the next opportunity and cannot use perform_later.
-
#
-
# N/A: The adapter does not support queueing.
-
#
-
# NOTE:
-
# queue_classic supports job scheduling since version 3.1.
-
# For older versions you can use the queue_classic-later gem.
-
#
-
# ==== Priorities
-
#
-
# The order in which jobs are processed can be configured differently depending
-
# on the adapter.
-
#
-
# Job: Any class inheriting from the adapter may set the priority on the job
-
# object relative to other jobs.
-
#
-
# Queue: The adapter can set the priority for job queues, when setting a queue
-
# with Active Job this will be respected.
-
#
-
# Yes: Allows the priority to be set on the job object, at the queue level or
-
# as default configuration option.
-
#
-
# No: Does not allow the priority of jobs to be configured.
-
#
-
# N/A: The adapter does not support queueing, and therefore sorting them.
-
#
-
# ==== Timeout
-
#
-
# When a job will stop after the allotted time.
-
#
-
# Job: The timeout can be set for each instance of the job class.
-
#
-
# Queue: The timeout is set for all jobs on the queue.
-
#
-
# Global: The adapter is configured that all jobs have a maximum run time.
-
#
-
# N/A: This adapter does not run in a separate process, and therefore timeout
-
# is unsupported.
-
#
-
# ==== Retries
-
#
-
# Job: The number of retries can be set per instance of the job class.
-
#
-
# Yes: The Number of retries can be configured globally, for each instance or
-
# on the queue. This adapter may also present failed instances of the job class
-
# that can be restarted.
-
#
-
# Global: The adapter has a global number of retries.
-
#
-
# N/A: The adapter does not run in a separate process, and therefore doesn't
-
# support retries.
-
#
-
# === Async and Inline Queue Adapters
-
#
-
# Active Job has two built-in queue adapters intended for development and
-
# testing: +:async+ and +:inline+.
-
1
module QueueAdapters
-
1
extend ActiveSupport::Autoload
-
-
1
autoload :AsyncAdapter
-
1
autoload :InlineAdapter
-
1
autoload :BackburnerAdapter
-
1
autoload :DelayedJobAdapter
-
1
autoload :QuAdapter
-
1
autoload :QueAdapter
-
1
autoload :QueueClassicAdapter
-
1
autoload :ResqueAdapter
-
1
autoload :SidekiqAdapter
-
1
autoload :SneakersAdapter
-
1
autoload :SuckerPunchAdapter
-
1
autoload :TestAdapter
-
-
1
ADAPTER = "Adapter".freeze
-
1
private_constant :ADAPTER
-
-
1
class << self
-
# Returns adapter for specified name.
-
#
-
# ActiveJob::QueueAdapters.lookup(:sidekiq)
-
# # => ActiveJob::QueueAdapters::SidekiqAdapter
-
1
def lookup(name)
-
2
const_get(name.to_s.camelize << ADAPTER)
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "securerandom"
-
1
require "concurrent/scheduled_task"
-
1
require "concurrent/executor/thread_pool_executor"
-
1
require "concurrent/utility/processor_counter"
-
-
1
module ActiveJob
-
1
module QueueAdapters
-
# == Active Job Async adapter
-
#
-
# The Async adapter runs jobs with an in-process thread pool.
-
#
-
# This is the default queue adapter. It's well-suited for dev/test since
-
# it doesn't need an external infrastructure, but it's a poor fit for
-
# production since it drops pending jobs on restart.
-
#
-
# To use this adapter, set queue adapter to +:async+:
-
#
-
# config.active_job.queue_adapter = :async
-
#
-
# To configure the adapter's thread pool, instantiate the adapter and
-
# pass your own config:
-
#
-
# config.active_job.queue_adapter = ActiveJob::QueueAdapters::AsyncAdapter.new \
-
# min_threads: 1,
-
# max_threads: 2 * Concurrent.processor_count,
-
# idletime: 600.seconds
-
#
-
# The adapter uses a {Concurrent Ruby}[https://github.com/ruby-concurrency/concurrent-ruby] thread pool to schedule and execute
-
# jobs. Since jobs share a single thread pool, long-running jobs will block
-
# short-lived jobs. Fine for dev/test; bad for production.
-
1
class AsyncAdapter
-
# See {Concurrent::ThreadPoolExecutor}[https://ruby-concurrency.github.io/concurrent-ruby/Concurrent/ThreadPoolExecutor.html] for executor options.
-
1
def initialize(**executor_options)
-
2
@scheduler = Scheduler.new(**executor_options)
-
end
-
-
1
def enqueue(job) #:nodoc:
-
@scheduler.enqueue JobWrapper.new(job), queue_name: job.queue_name
-
end
-
-
1
def enqueue_at(job, timestamp) #:nodoc:
-
@scheduler.enqueue_at JobWrapper.new(job), timestamp, queue_name: job.queue_name
-
end
-
-
# Gracefully stop processing jobs. Finishes in-progress work and handles
-
# any new jobs following the executor's fallback policy (`caller_runs`).
-
# Waits for termination by default. Pass `wait: false` to continue.
-
1
def shutdown(wait: true) #:nodoc:
-
@scheduler.shutdown wait: wait
-
end
-
-
# Used for our test suite.
-
1
def immediate=(immediate) #:nodoc:
-
@scheduler.immediate = immediate
-
end
-
-
# Note that we don't actually need to serialize the jobs since we're
-
# performing them in-process, but we do so anyway for parity with other
-
# adapters and deployment environments. Otherwise, serialization bugs
-
# may creep in undetected.
-
1
class JobWrapper #:nodoc:
-
1
def initialize(job)
-
job.provider_job_id = SecureRandom.uuid
-
@job_data = job.serialize
-
end
-
-
1
def perform
-
Base.execute @job_data
-
end
-
end
-
-
1
class Scheduler #:nodoc:
-
DEFAULT_EXECUTOR_OPTIONS = {
-
min_threads: 0,
-
max_threads: Concurrent.processor_count,
-
auto_terminate: true,
-
idletime: 60, # 1 minute
-
max_queue: 0, # unlimited
-
fallback_policy: :caller_runs # shouldn't matter -- 0 max queue
-
}.freeze
-
-
1
attr_accessor :immediate
-
-
1
def initialize(**options)
-
2
self.immediate = false
-
2
@immediate_executor = Concurrent::ImmediateExecutor.new
-
2
@async_executor = Concurrent::ThreadPoolExecutor.new(DEFAULT_EXECUTOR_OPTIONS.merge(options))
-
end
-
-
1
def enqueue(job, queue_name:)
-
executor.post(job, &:perform)
-
end
-
-
1
def enqueue_at(job, timestamp, queue_name:)
-
delay = timestamp - Time.current.to_f
-
if delay > 0
-
Concurrent::ScheduledTask.execute(delay, args: [job], executor: executor, &:perform)
-
else
-
enqueue(job, queue_name: queue_name)
-
end
-
end
-
-
1
def shutdown(wait: true)
-
@async_executor.shutdown
-
@async_executor.wait_for_termination if wait
-
end
-
-
1
def executor
-
immediate ? @immediate_executor : @async_executor
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActiveJob
-
1
module QueueName
-
1
extend ActiveSupport::Concern
-
-
# Includes the ability to override the default queue name and prefix.
-
1
module ClassMethods
-
1
mattr_accessor :queue_name_prefix
-
1
mattr_accessor :default_queue_name, default: "default"
-
-
# Specifies the name of the queue to process the job on.
-
#
-
# class PublishToFeedJob < ActiveJob::Base
-
# queue_as :feeds
-
#
-
# def perform(post)
-
# post.to_feed!
-
# end
-
# end
-
1
def queue_as(part_name = nil, &block)
-
1
if block_given?
-
1
self.queue_name = block
-
else
-
self.queue_name = queue_name_from_part(part_name)
-
end
-
end
-
-
1
def queue_name_from_part(part_name) #:nodoc:
-
queue_name = part_name || default_queue_name
-
name_parts = [queue_name_prefix.presence, queue_name]
-
name_parts.compact.join(queue_name_delimiter)
-
end
-
end
-
-
1
included do
-
1
class_attribute :queue_name, instance_accessor: false, default: default_queue_name
-
1
class_attribute :queue_name_delimiter, instance_accessor: false, default: "_"
-
end
-
-
# Returns the name of the queue the job will be run on.
-
1
def queue_name
-
if @queue_name.is_a?(Proc)
-
@queue_name = self.class.queue_name_from_part(instance_exec(&@queue_name))
-
end
-
@queue_name
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActiveJob
-
1
module QueuePriority
-
1
extend ActiveSupport::Concern
-
-
# Includes the ability to override the default queue priority.
-
1
module ClassMethods
-
1
mattr_accessor :default_priority
-
-
# Specifies the priority of the queue to create the job with.
-
#
-
# class PublishToFeedJob < ActiveJob::Base
-
# queue_with_priority 50
-
#
-
# def perform(post)
-
# post.to_feed!
-
# end
-
# end
-
#
-
# Specify either an argument or a block.
-
1
def queue_with_priority(priority = nil, &block)
-
if block_given?
-
self.priority = block
-
else
-
self.priority = priority
-
end
-
end
-
end
-
-
1
included do
-
1
class_attribute :priority, instance_accessor: false, default: default_priority
-
end
-
-
# Returns the priority that the job will be created with
-
1
def priority
-
if @priority.is_a?(Proc)
-
@priority = instance_exec(&@priority)
-
end
-
@priority
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_support/core_ext/class/subclasses"
-
1
require "active_support/core_ext/hash/keys"
-
-
1
module ActiveJob
-
# Provides helper methods for testing Active Job
-
1
module TestHelper
-
1
delegate :enqueued_jobs, :enqueued_jobs=,
-
:performed_jobs, :performed_jobs=,
-
to: :queue_adapter
-
-
1
module TestQueueAdapter
-
1
extend ActiveSupport::Concern
-
-
1
included do
-
1
class_attribute :_test_adapter, instance_accessor: false, instance_predicate: false
-
end
-
-
1
module ClassMethods
-
1
def queue_adapter
-
self._test_adapter.nil? ? super : self._test_adapter
-
end
-
-
1
def disable_test_adapter
-
self._test_adapter = nil
-
end
-
-
1
def enable_test_adapter(test_adapter)
-
self._test_adapter = test_adapter
-
end
-
end
-
end
-
-
1
ActiveJob::Base.include(TestQueueAdapter)
-
-
1
def before_setup # :nodoc:
-
test_adapter = queue_adapter_for_test
-
-
queue_adapter_changed_jobs.each do |klass|
-
klass.enable_test_adapter(test_adapter)
-
end
-
-
clear_enqueued_jobs
-
clear_performed_jobs
-
super
-
end
-
-
1
def after_teardown # :nodoc:
-
super
-
-
queue_adapter_changed_jobs.each { |klass| klass.disable_test_adapter }
-
end
-
-
# Specifies the queue adapter to use with all active job test helpers.
-
#
-
# Returns an instance of the queue adapter and defaults to
-
# <tt>ActiveJob::QueueAdapters::TestAdapter</tt>.
-
#
-
# Note: The adapter provided by this method must provide some additional
-
# methods from those expected of a standard <tt>ActiveJob::QueueAdapter</tt>
-
# in order to be used with the active job test helpers. Refer to
-
# <tt>ActiveJob::QueueAdapters::TestAdapter</tt>.
-
1
def queue_adapter_for_test
-
ActiveJob::QueueAdapters::TestAdapter.new
-
end
-
-
# Asserts that the number of enqueued jobs matches the given number.
-
#
-
# def test_jobs
-
# assert_enqueued_jobs 0
-
# HelloJob.perform_later('david')
-
# assert_enqueued_jobs 1
-
# HelloJob.perform_later('abdelkader')
-
# assert_enqueued_jobs 2
-
# end
-
#
-
# If a block is passed, that block will cause the specified number of
-
# jobs to be enqueued.
-
#
-
# def test_jobs_again
-
# assert_enqueued_jobs 1 do
-
# HelloJob.perform_later('cristian')
-
# end
-
#
-
# assert_enqueued_jobs 2 do
-
# HelloJob.perform_later('aaron')
-
# HelloJob.perform_later('rafael')
-
# end
-
# end
-
#
-
# The number of times a specific job was enqueued can be asserted.
-
#
-
# def test_logging_job
-
# assert_enqueued_jobs 1, only: LoggingJob do
-
# LoggingJob.perform_later
-
# HelloJob.perform_later('jeremy')
-
# end
-
# end
-
#
-
# The number of times a job except specific class was enqueued can be asserted.
-
#
-
# def test_logging_job
-
# assert_enqueued_jobs 1, except: HelloJob do
-
# LoggingJob.perform_later
-
# HelloJob.perform_later('jeremy')
-
# end
-
# end
-
#
-
# The number of times a job is enqueued to a specific queue can also be asserted.
-
#
-
# def test_logging_job
-
# assert_enqueued_jobs 2, queue: 'default' do
-
# LoggingJob.perform_later
-
# HelloJob.perform_later('elfassy')
-
# end
-
# end
-
1
def assert_enqueued_jobs(number, only: nil, except: nil, queue: nil)
-
if block_given?
-
original_count = enqueued_jobs_size(only: only, except: except, queue: queue)
-
yield
-
new_count = enqueued_jobs_size(only: only, except: except, queue: queue)
-
assert_equal number, new_count - original_count, "#{number} jobs expected, but #{new_count - original_count} were enqueued"
-
else
-
actual_count = enqueued_jobs_size(only: only, except: except, queue: queue)
-
assert_equal number, actual_count, "#{number} jobs expected, but #{actual_count} were enqueued"
-
end
-
end
-
-
# Asserts that no jobs have been enqueued.
-
#
-
# def test_jobs
-
# assert_no_enqueued_jobs
-
# HelloJob.perform_later('jeremy')
-
# assert_enqueued_jobs 1
-
# end
-
#
-
# If a block is passed, that block should not cause any job to be enqueued.
-
#
-
# def test_jobs_again
-
# assert_no_enqueued_jobs do
-
# # No job should be enqueued from this block
-
# end
-
# end
-
#
-
# It can be asserted that no jobs of a specific kind are enqueued:
-
#
-
# def test_no_logging
-
# assert_no_enqueued_jobs only: LoggingJob do
-
# HelloJob.perform_later('jeremy')
-
# end
-
# end
-
#
-
# It can be asserted that no jobs except specific class are enqueued:
-
#
-
# def test_no_logging
-
# assert_no_enqueued_jobs except: HelloJob do
-
# HelloJob.perform_later('jeremy')
-
# end
-
# end
-
#
-
# Note: This assertion is simply a shortcut for:
-
#
-
# assert_enqueued_jobs 0, &block
-
1
def assert_no_enqueued_jobs(only: nil, except: nil, &block)
-
assert_enqueued_jobs 0, only: only, except: except, &block
-
end
-
-
# Asserts that the number of performed jobs matches the given number.
-
# If no block is passed, <tt>perform_enqueued_jobs</tt>
-
# must be called around the job call.
-
#
-
# def test_jobs
-
# assert_performed_jobs 0
-
#
-
# perform_enqueued_jobs do
-
# HelloJob.perform_later('xavier')
-
# end
-
# assert_performed_jobs 1
-
#
-
# perform_enqueued_jobs do
-
# HelloJob.perform_later('yves')
-
# assert_performed_jobs 2
-
# end
-
# end
-
#
-
# If a block is passed, that block should cause the specified number of
-
# jobs to be performed.
-
#
-
# def test_jobs_again
-
# assert_performed_jobs 1 do
-
# HelloJob.perform_later('robin')
-
# end
-
#
-
# assert_performed_jobs 2 do
-
# HelloJob.perform_later('carlos')
-
# HelloJob.perform_later('sean')
-
# end
-
# end
-
#
-
# The block form supports filtering. If the :only option is specified,
-
# then only the listed job(s) will be performed.
-
#
-
# def test_hello_job
-
# assert_performed_jobs 1, only: HelloJob do
-
# HelloJob.perform_later('jeremy')
-
# LoggingJob.perform_later
-
# end
-
# end
-
#
-
# Also if the :except option is specified,
-
# then the job(s) except specific class will be performed.
-
#
-
# def test_hello_job
-
# assert_performed_jobs 1, except: LoggingJob do
-
# HelloJob.perform_later('jeremy')
-
# LoggingJob.perform_later
-
# end
-
# end
-
#
-
# An array may also be specified, to support testing multiple jobs.
-
#
-
# def test_hello_and_logging_jobs
-
# assert_nothing_raised do
-
# assert_performed_jobs 2, only: [HelloJob, LoggingJob] do
-
# HelloJob.perform_later('jeremy')
-
# LoggingJob.perform_later('stewie')
-
# RescueJob.perform_later('david')
-
# end
-
# end
-
# end
-
1
def assert_performed_jobs(number, only: nil, except: nil)
-
if block_given?
-
original_count = performed_jobs.size
-
perform_enqueued_jobs(only: only, except: except) { yield }
-
new_count = performed_jobs.size
-
assert_equal number, new_count - original_count,
-
"#{number} jobs expected, but #{new_count - original_count} were performed"
-
else
-
performed_jobs_size = performed_jobs.size
-
assert_equal number, performed_jobs_size, "#{number} jobs expected, but #{performed_jobs_size} were performed"
-
end
-
end
-
-
# Asserts that no jobs have been performed.
-
#
-
# def test_jobs
-
# assert_no_performed_jobs
-
#
-
# perform_enqueued_jobs do
-
# HelloJob.perform_later('matthew')
-
# assert_performed_jobs 1
-
# end
-
# end
-
#
-
# If a block is passed, that block should not cause any job to be performed.
-
#
-
# def test_jobs_again
-
# assert_no_performed_jobs do
-
# # No job should be performed from this block
-
# end
-
# end
-
#
-
# The block form supports filtering. If the :only option is specified,
-
# then only the listed job(s) will not be performed.
-
#
-
# def test_no_logging
-
# assert_no_performed_jobs only: LoggingJob do
-
# HelloJob.perform_later('jeremy')
-
# end
-
# end
-
#
-
# Also if the :except option is specified,
-
# then the job(s) except specific class will not be performed.
-
#
-
# def test_no_logging
-
# assert_no_performed_jobs except: HelloJob do
-
# HelloJob.perform_later('jeremy')
-
# end
-
# end
-
#
-
# Note: This assertion is simply a shortcut for:
-
#
-
# assert_performed_jobs 0, &block
-
1
def assert_no_performed_jobs(only: nil, except: nil, &block)
-
assert_performed_jobs 0, only: only, except: except, &block
-
end
-
-
# Asserts that the job passed in the block has been enqueued with the given arguments.
-
#
-
# def test_assert_enqueued_with
-
# assert_enqueued_with(job: MyJob, args: [1,2,3], queue: 'low') do
-
# MyJob.perform_later(1,2,3)
-
# end
-
#
-
# assert_enqueued_with(job: MyJob, at: Date.tomorrow.noon) do
-
# MyJob.set(wait_until: Date.tomorrow.noon).perform_later
-
# end
-
# end
-
1
def assert_enqueued_with(job: nil, args: nil, at: nil, queue: nil)
-
original_enqueued_jobs_count = enqueued_jobs.count
-
expected = { job: job, args: args, at: at, queue: queue }.compact
-
expected_args = prepare_args_for_assertion(expected)
-
yield
-
in_block_jobs = enqueued_jobs.drop(original_enqueued_jobs_count)
-
matching_job = in_block_jobs.find do |in_block_job|
-
deserialized_job = deserialize_args_for_assertion(in_block_job)
-
expected_args.all? { |key, value| value == deserialized_job[key] }
-
end
-
assert matching_job, "No enqueued job found with #{expected}"
-
instantiate_job(matching_job)
-
end
-
-
# Asserts that the job passed in the block has been performed with the given arguments.
-
#
-
# def test_assert_performed_with
-
# assert_performed_with(job: MyJob, args: [1,2,3], queue: 'high') do
-
# MyJob.perform_later(1,2,3)
-
# end
-
#
-
# assert_performed_with(job: MyJob, at: Date.tomorrow.noon) do
-
# MyJob.set(wait_until: Date.tomorrow.noon).perform_later
-
# end
-
# end
-
1
def assert_performed_with(job: nil, args: nil, at: nil, queue: nil)
-
original_performed_jobs_count = performed_jobs.count
-
expected = { job: job, args: args, at: at, queue: queue }.compact
-
expected_args = prepare_args_for_assertion(expected)
-
perform_enqueued_jobs { yield }
-
in_block_jobs = performed_jobs.drop(original_performed_jobs_count)
-
matching_job = in_block_jobs.find do |in_block_job|
-
deserialized_job = deserialize_args_for_assertion(in_block_job)
-
expected_args.all? { |key, value| value == deserialized_job[key] }
-
end
-
assert matching_job, "No performed job found with #{expected}"
-
instantiate_job(matching_job)
-
end
-
-
# Performs all enqueued jobs in the duration of the block.
-
#
-
# def test_perform_enqueued_jobs
-
# perform_enqueued_jobs do
-
# MyJob.perform_later(1, 2, 3)
-
# end
-
# assert_performed_jobs 1
-
# end
-
#
-
# This method also supports filtering. If the +:only+ option is specified,
-
# then only the listed job(s) will be performed.
-
#
-
# def test_perform_enqueued_jobs_with_only
-
# perform_enqueued_jobs(only: MyJob) do
-
# MyJob.perform_later(1, 2, 3) # will be performed
-
# HelloJob.perform_later(1, 2, 3) # will not be performed
-
# end
-
# assert_performed_jobs 1
-
# end
-
#
-
# Also if the +:except+ option is specified,
-
# then the job(s) except specific class will be performed.
-
#
-
# def test_perform_enqueued_jobs_with_except
-
# perform_enqueued_jobs(except: HelloJob) do
-
# MyJob.perform_later(1, 2, 3) # will be performed
-
# HelloJob.perform_later(1, 2, 3) # will not be performed
-
# end
-
# assert_performed_jobs 1
-
# end
-
#
-
1
def perform_enqueued_jobs(only: nil, except: nil)
-
validate_option(only: only, except: except)
-
old_perform_enqueued_jobs = queue_adapter.perform_enqueued_jobs
-
old_perform_enqueued_at_jobs = queue_adapter.perform_enqueued_at_jobs
-
old_filter = queue_adapter.filter
-
old_reject = queue_adapter.reject
-
-
begin
-
queue_adapter.perform_enqueued_jobs = true
-
queue_adapter.perform_enqueued_at_jobs = true
-
queue_adapter.filter = only
-
queue_adapter.reject = except
-
yield
-
ensure
-
queue_adapter.perform_enqueued_jobs = old_perform_enqueued_jobs
-
queue_adapter.perform_enqueued_at_jobs = old_perform_enqueued_at_jobs
-
queue_adapter.filter = old_filter
-
queue_adapter.reject = old_reject
-
end
-
end
-
-
# Accesses the queue_adapter set by ActiveJob::Base.
-
#
-
# def test_assert_job_has_custom_queue_adapter_set
-
# assert_instance_of CustomQueueAdapter, HelloJob.queue_adapter
-
# end
-
1
def queue_adapter
-
ActiveJob::Base.queue_adapter
-
end
-
-
1
private
-
1
def clear_enqueued_jobs
-
enqueued_jobs.clear
-
end
-
-
1
def clear_performed_jobs
-
performed_jobs.clear
-
end
-
-
1
def enqueued_jobs_size(only: nil, except: nil, queue: nil)
-
validate_option(only: only, except: except)
-
enqueued_jobs.count do |job|
-
job_class = job.fetch(:job)
-
if only
-
next false unless Array(only).include?(job_class)
-
elsif except
-
next false if Array(except).include?(job_class)
-
end
-
if queue
-
next false unless queue.to_s == job.fetch(:queue, job_class.queue_name)
-
end
-
true
-
end
-
end
-
-
1
def prepare_args_for_assertion(args)
-
args.dup.tap do |arguments|
-
arguments[:at] = arguments[:at].to_f if arguments[:at]
-
end
-
end
-
-
1
def deserialize_args_for_assertion(job)
-
job.dup.tap do |new_job|
-
new_job[:args] = ActiveJob::Arguments.deserialize(new_job[:args]) if new_job[:args]
-
end
-
end
-
-
1
def instantiate_job(payload)
-
args = ActiveJob::Arguments.deserialize(payload[:args])
-
job = payload[:job].new(*args)
-
job.scheduled_at = Time.at(payload[:at]) if payload.key?(:at)
-
job.queue_name = payload[:queue]
-
job
-
end
-
-
1
def queue_adapter_changed_jobs
-
(ActiveJob::Base.descendants << ActiveJob::Base).select do |klass|
-
# only override explicitly set adapters, a quirk of `class_attribute`
-
klass.singleton_class.public_instance_methods(false).include?(:_queue_adapter)
-
end
-
end
-
-
1
def validate_option(only: nil, except: nil)
-
raise ArgumentError, "Cannot specify both `:only` and `:except` options." if only && except
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActiveJob
-
1
module Translation #:nodoc:
-
1
extend ActiveSupport::Concern
-
-
1
included do
-
1
around_perform do |job, block, _|
-
I18n.with_locale(job.locale, &block)
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActiveRecord
-
1
class AssociationRelation < Relation
-
1
def initialize(klass, association)
-
super(klass)
-
@association = association
-
end
-
-
1
def proxy_association
-
@association
-
end
-
-
1
def ==(other)
-
other == records
-
end
-
-
1
def build(*args, &block)
-
scoping { @association.build(*args, &block) }
-
end
-
1
alias new build
-
-
1
def create(*args, &block)
-
scoping { @association.create(*args, &block) }
-
end
-
-
1
def create!(*args, &block)
-
scoping { @association.create!(*args, &block) }
-
end
-
-
1
private
-
-
1
def exec_queries
-
super do |record|
-
@association.set_inverse_instance_from_queries(record)
-
yield record if block_given?
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActiveRecord
-
1
module Associations
-
# Association proxies in Active Record are middlemen between the object that
-
# holds the association, known as the <tt>@owner</tt>, and the actual associated
-
# object, known as the <tt>@target</tt>. The kind of association any proxy is
-
# about is available in <tt>@reflection</tt>. That's an instance of the class
-
# ActiveRecord::Reflection::AssociationReflection.
-
#
-
# For example, given
-
#
-
# class Blog < ActiveRecord::Base
-
# has_many :posts
-
# end
-
#
-
# blog = Blog.first
-
#
-
# the association proxy in <tt>blog.posts</tt> has the object in +blog+ as
-
# <tt>@owner</tt>, the collection of its posts as <tt>@target</tt>, and
-
# the <tt>@reflection</tt> object represents a <tt>:has_many</tt> macro.
-
#
-
# This class delegates unknown methods to <tt>@target</tt> via
-
# <tt>method_missing</tt>.
-
#
-
# The <tt>@target</tt> object is not \loaded until needed. For example,
-
#
-
# blog.posts.count
-
#
-
# is computed directly through SQL and does not trigger by itself the
-
# instantiation of the actual post records.
-
1
class CollectionProxy < Relation
-
1
def initialize(klass, association) #:nodoc:
-
@association = association
-
super klass
-
-
extensions = association.extensions
-
extend(*extensions) if extensions.any?
-
end
-
-
1
def target
-
@association.target
-
end
-
-
1
def load_target
-
@association.load_target
-
end
-
-
# Returns +true+ if the association has been loaded, otherwise +false+.
-
#
-
# person.pets.loaded? # => false
-
# person.pets
-
# person.pets.loaded? # => true
-
1
def loaded?
-
@association.loaded?
-
end
-
-
##
-
# :method: select
-
#
-
# :call-seq:
-
# select(*fields, &block)
-
#
-
# Works in two ways.
-
#
-
# *First:* Specify a subset of fields to be selected from the result set.
-
#
-
# class Person < ActiveRecord::Base
-
# has_many :pets
-
# end
-
#
-
# person.pets
-
# # => [
-
# # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
-
# # #<Pet id: 2, name: "Spook", person_id: 1>,
-
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
-
# # ]
-
#
-
# person.pets.select(:name)
-
# # => [
-
# # #<Pet id: nil, name: "Fancy-Fancy">,
-
# # #<Pet id: nil, name: "Spook">,
-
# # #<Pet id: nil, name: "Choo-Choo">
-
# # ]
-
#
-
# person.pets.select(:id, :name)
-
# # => [
-
# # #<Pet id: 1, name: "Fancy-Fancy">,
-
# # #<Pet id: 2, name: "Spook">,
-
# # #<Pet id: 3, name: "Choo-Choo">
-
# # ]
-
#
-
# Be careful because this also means you're initializing a model
-
# object with only the fields that you've selected. If you attempt
-
# to access a field except +id+ that is not in the initialized record you'll
-
# receive:
-
#
-
# person.pets.select(:name).first.person_id
-
# # => ActiveModel::MissingAttributeError: missing attribute: person_id
-
#
-
# *Second:* You can pass a block so it can be used just like Array#select.
-
# This builds an array of objects from the database for the scope,
-
# converting them into an array and iterating through them using
-
# Array#select.
-
#
-
# person.pets.select { |pet| pet.name =~ /oo/ }
-
# # => [
-
# # #<Pet id: 2, name: "Spook", person_id: 1>,
-
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
-
# # ]
-
-
# Finds an object in the collection responding to the +id+. Uses the same
-
# rules as ActiveRecord::Base.find. Returns ActiveRecord::RecordNotFound
-
# error if the object cannot be found.
-
#
-
# class Person < ActiveRecord::Base
-
# has_many :pets
-
# end
-
#
-
# person.pets
-
# # => [
-
# # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
-
# # #<Pet id: 2, name: "Spook", person_id: 1>,
-
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
-
# # ]
-
#
-
# person.pets.find(1) # => #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>
-
# person.pets.find(4) # => ActiveRecord::RecordNotFound: Couldn't find Pet with 'id'=4
-
#
-
# person.pets.find(2) { |pet| pet.name.downcase! }
-
# # => #<Pet id: 2, name: "fancy-fancy", person_id: 1>
-
#
-
# person.pets.find(2, 3)
-
# # => [
-
# # #<Pet id: 2, name: "Spook", person_id: 1>,
-
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
-
# # ]
-
1
def find(*args)
-
return super if block_given?
-
@association.find(*args)
-
end
-
-
##
-
# :method: first
-
#
-
# :call-seq:
-
# first(limit = nil)
-
#
-
# Returns the first record, or the first +n+ records, from the collection.
-
# If the collection is empty, the first form returns +nil+, and the second
-
# form returns an empty array.
-
#
-
# class Person < ActiveRecord::Base
-
# has_many :pets
-
# end
-
#
-
# person.pets
-
# # => [
-
# # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
-
# # #<Pet id: 2, name: "Spook", person_id: 1>,
-
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
-
# # ]
-
#
-
# person.pets.first # => #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>
-
#
-
# person.pets.first(2)
-
# # => [
-
# # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
-
# # #<Pet id: 2, name: "Spook", person_id: 1>
-
# # ]
-
#
-
# another_person_without.pets # => []
-
# another_person_without.pets.first # => nil
-
# another_person_without.pets.first(3) # => []
-
-
##
-
# :method: second
-
#
-
# :call-seq:
-
# second()
-
#
-
# Same as #first except returns only the second record.
-
-
##
-
# :method: third
-
#
-
# :call-seq:
-
# third()
-
#
-
# Same as #first except returns only the third record.
-
-
##
-
# :method: fourth
-
#
-
# :call-seq:
-
# fourth()
-
#
-
# Same as #first except returns only the fourth record.
-
-
##
-
# :method: fifth
-
#
-
# :call-seq:
-
# fifth()
-
#
-
# Same as #first except returns only the fifth record.
-
-
##
-
# :method: forty_two
-
#
-
# :call-seq:
-
# forty_two()
-
#
-
# Same as #first except returns only the forty second record.
-
# Also known as accessing "the reddit".
-
-
##
-
# :method: third_to_last
-
#
-
# :call-seq:
-
# third_to_last()
-
#
-
# Same as #first except returns only the third-to-last record.
-
-
##
-
# :method: second_to_last
-
#
-
# :call-seq:
-
# second_to_last()
-
#
-
# Same as #first except returns only the second-to-last record.
-
-
# Returns the last record, or the last +n+ records, from the collection.
-
# If the collection is empty, the first form returns +nil+, and the second
-
# form returns an empty array.
-
#
-
# class Person < ActiveRecord::Base
-
# has_many :pets
-
# end
-
#
-
# person.pets
-
# # => [
-
# # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
-
# # #<Pet id: 2, name: "Spook", person_id: 1>,
-
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
-
# # ]
-
#
-
# person.pets.last # => #<Pet id: 3, name: "Choo-Choo", person_id: 1>
-
#
-
# person.pets.last(2)
-
# # => [
-
# # #<Pet id: 2, name: "Spook", person_id: 1>,
-
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
-
# # ]
-
#
-
# another_person_without.pets # => []
-
# another_person_without.pets.last # => nil
-
# another_person_without.pets.last(3) # => []
-
1
def last(limit = nil)
-
load_target if find_from_target?
-
super
-
end
-
-
# Gives a record (or N records if a parameter is supplied) from the collection
-
# using the same rules as <tt>ActiveRecord::Base.take</tt>.
-
#
-
# class Person < ActiveRecord::Base
-
# has_many :pets
-
# end
-
#
-
# person.pets
-
# # => [
-
# # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
-
# # #<Pet id: 2, name: "Spook", person_id: 1>,
-
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
-
# # ]
-
#
-
# person.pets.take # => #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>
-
#
-
# person.pets.take(2)
-
# # => [
-
# # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
-
# # #<Pet id: 2, name: "Spook", person_id: 1>
-
# # ]
-
#
-
# another_person_without.pets # => []
-
# another_person_without.pets.take # => nil
-
# another_person_without.pets.take(2) # => []
-
1
def take(limit = nil)
-
load_target if find_from_target?
-
super
-
end
-
-
# Returns a new object of the collection type that has been instantiated
-
# with +attributes+ and linked to this object, but have not yet been saved.
-
# You can pass an array of attributes hashes, this will return an array
-
# with the new objects.
-
#
-
# class Person
-
# has_many :pets
-
# end
-
#
-
# person.pets.build
-
# # => #<Pet id: nil, name: nil, person_id: 1>
-
#
-
# person.pets.build(name: 'Fancy-Fancy')
-
# # => #<Pet id: nil, name: "Fancy-Fancy", person_id: 1>
-
#
-
# person.pets.build([{name: 'Spook'}, {name: 'Choo-Choo'}, {name: 'Brain'}])
-
# # => [
-
# # #<Pet id: nil, name: "Spook", person_id: 1>,
-
# # #<Pet id: nil, name: "Choo-Choo", person_id: 1>,
-
# # #<Pet id: nil, name: "Brain", person_id: 1>
-
# # ]
-
#
-
# person.pets.size # => 5 # size of the collection
-
# person.pets.count # => 0 # count from database
-
1
def build(attributes = {}, &block)
-
@association.build(attributes, &block)
-
end
-
1
alias_method :new, :build
-
-
# Returns a new object of the collection type that has been instantiated with
-
# attributes, linked to this object and that has already been saved (if it
-
# passes the validations).
-
#
-
# class Person
-
# has_many :pets
-
# end
-
#
-
# person.pets.create(name: 'Fancy-Fancy')
-
# # => #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>
-
#
-
# person.pets.create([{name: 'Spook'}, {name: 'Choo-Choo'}])
-
# # => [
-
# # #<Pet id: 2, name: "Spook", person_id: 1>,
-
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
-
# # ]
-
#
-
# person.pets.size # => 3
-
# person.pets.count # => 3
-
#
-
# person.pets.find(1, 2, 3)
-
# # => [
-
# # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
-
# # #<Pet id: 2, name: "Spook", person_id: 1>,
-
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
-
# # ]
-
1
def create(attributes = {}, &block)
-
@association.create(attributes, &block)
-
end
-
-
# Like #create, except that if the record is invalid, raises an exception.
-
#
-
# class Person
-
# has_many :pets
-
# end
-
#
-
# class Pet
-
# validates :name, presence: true
-
# end
-
#
-
# person.pets.create!(name: nil)
-
# # => ActiveRecord::RecordInvalid: Validation failed: Name can't be blank
-
1
def create!(attributes = {}, &block)
-
@association.create!(attributes, &block)
-
end
-
-
# Replaces this collection with +other_array+. This will perform a diff
-
# and delete/add only records that have changed.
-
#
-
# class Person < ActiveRecord::Base
-
# has_many :pets
-
# end
-
#
-
# person.pets
-
# # => [#<Pet id: 1, name: "Gorby", group: "cats", person_id: 1>]
-
#
-
# other_pets = [Pet.new(name: 'Puff', group: 'celebrities']
-
#
-
# person.pets.replace(other_pets)
-
#
-
# person.pets
-
# # => [#<Pet id: 2, name: "Puff", group: "celebrities", person_id: 1>]
-
#
-
# If the supplied array has an incorrect association type, it raises
-
# an <tt>ActiveRecord::AssociationTypeMismatch</tt> error:
-
#
-
# person.pets.replace(["doo", "ggie", "gaga"])
-
# # => ActiveRecord::AssociationTypeMismatch: Pet expected, got String
-
1
def replace(other_array)
-
@association.replace(other_array)
-
end
-
-
# Deletes all the records from the collection according to the strategy
-
# specified by the +:dependent+ option. If no +:dependent+ option is given,
-
# then it will follow the default strategy.
-
#
-
# For <tt>has_many :through</tt> associations, the default deletion strategy is
-
# +:delete_all+.
-
#
-
# For +has_many+ associations, the default deletion strategy is +:nullify+.
-
# This sets the foreign keys to +NULL+.
-
#
-
# class Person < ActiveRecord::Base
-
# has_many :pets # dependent: :nullify option by default
-
# end
-
#
-
# person.pets.size # => 3
-
# person.pets
-
# # => [
-
# # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
-
# # #<Pet id: 2, name: "Spook", person_id: 1>,
-
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
-
# # ]
-
#
-
# person.pets.delete_all
-
# # => [
-
# # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
-
# # #<Pet id: 2, name: "Spook", person_id: 1>,
-
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
-
# # ]
-
#
-
# person.pets.size # => 0
-
# person.pets # => []
-
#
-
# Pet.find(1, 2, 3)
-
# # => [
-
# # #<Pet id: 1, name: "Fancy-Fancy", person_id: nil>,
-
# # #<Pet id: 2, name: "Spook", person_id: nil>,
-
# # #<Pet id: 3, name: "Choo-Choo", person_id: nil>
-
# # ]
-
#
-
# Both +has_many+ and <tt>has_many :through</tt> dependencies default to the
-
# +:delete_all+ strategy if the +:dependent+ option is set to +:destroy+.
-
# Records are not instantiated and callbacks will not be fired.
-
#
-
# class Person < ActiveRecord::Base
-
# has_many :pets, dependent: :destroy
-
# end
-
#
-
# person.pets.size # => 3
-
# person.pets
-
# # => [
-
# # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
-
# # #<Pet id: 2, name: "Spook", person_id: 1>,
-
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
-
# # ]
-
#
-
# person.pets.delete_all
-
#
-
# Pet.find(1, 2, 3)
-
# # => ActiveRecord::RecordNotFound: Couldn't find all Pets with 'id': (1, 2, 3)
-
#
-
# If it is set to <tt>:delete_all</tt>, all the objects are deleted
-
# *without* calling their +destroy+ method.
-
#
-
# class Person < ActiveRecord::Base
-
# has_many :pets, dependent: :delete_all
-
# end
-
#
-
# person.pets.size # => 3
-
# person.pets
-
# # => [
-
# # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
-
# # #<Pet id: 2, name: "Spook", person_id: 1>,
-
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
-
# # ]
-
#
-
# person.pets.delete_all
-
#
-
# Pet.find(1, 2, 3)
-
# # => ActiveRecord::RecordNotFound: Couldn't find all Pets with 'id': (1, 2, 3)
-
1
def delete_all(dependent = nil)
-
@association.delete_all(dependent).tap { reset_scope }
-
end
-
-
# Deletes the records of the collection directly from the database
-
# ignoring the +:dependent+ option. Records are instantiated and it
-
# invokes +before_remove+, +after_remove+ , +before_destroy+ and
-
# +after_destroy+ callbacks.
-
#
-
# class Person < ActiveRecord::Base
-
# has_many :pets
-
# end
-
#
-
# person.pets.size # => 3
-
# person.pets
-
# # => [
-
# # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
-
# # #<Pet id: 2, name: "Spook", person_id: 1>,
-
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
-
# # ]
-
#
-
# person.pets.destroy_all
-
#
-
# person.pets.size # => 0
-
# person.pets # => []
-
#
-
# Pet.find(1) # => Couldn't find Pet with id=1
-
1
def destroy_all
-
@association.destroy_all.tap { reset_scope }
-
end
-
-
# Deletes the +records+ supplied from the collection according to the strategy
-
# specified by the +:dependent+ option. If no +:dependent+ option is given,
-
# then it will follow the default strategy. Returns an array with the
-
# deleted records.
-
#
-
# For <tt>has_many :through</tt> associations, the default deletion strategy is
-
# +:delete_all+.
-
#
-
# For +has_many+ associations, the default deletion strategy is +:nullify+.
-
# This sets the foreign keys to +NULL+.
-
#
-
# class Person < ActiveRecord::Base
-
# has_many :pets # dependent: :nullify option by default
-
# end
-
#
-
# person.pets.size # => 3
-
# person.pets
-
# # => [
-
# # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
-
# # #<Pet id: 2, name: "Spook", person_id: 1>,
-
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
-
# # ]
-
#
-
# person.pets.delete(Pet.find(1))
-
# # => [#<Pet id: 1, name: "Fancy-Fancy", person_id: 1>]
-
#
-
# person.pets.size # => 2
-
# person.pets
-
# # => [
-
# # #<Pet id: 2, name: "Spook", person_id: 1>,
-
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
-
# # ]
-
#
-
# Pet.find(1)
-
# # => #<Pet id: 1, name: "Fancy-Fancy", person_id: nil>
-
#
-
# If it is set to <tt>:destroy</tt> all the +records+ are removed by calling
-
# their +destroy+ method. See +destroy+ for more information.
-
#
-
# class Person < ActiveRecord::Base
-
# has_many :pets, dependent: :destroy
-
# end
-
#
-
# person.pets.size # => 3
-
# person.pets
-
# # => [
-
# # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
-
# # #<Pet id: 2, name: "Spook", person_id: 1>,
-
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
-
# # ]
-
#
-
# person.pets.delete(Pet.find(1), Pet.find(3))
-
# # => [
-
# # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
-
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
-
# # ]
-
#
-
# person.pets.size # => 1
-
# person.pets
-
# # => [#<Pet id: 2, name: "Spook", person_id: 1>]
-
#
-
# Pet.find(1, 3)
-
# # => ActiveRecord::RecordNotFound: Couldn't find all Pets with 'id': (1, 3)
-
#
-
# If it is set to <tt>:delete_all</tt>, all the +records+ are deleted
-
# *without* calling their +destroy+ method.
-
#
-
# class Person < ActiveRecord::Base
-
# has_many :pets, dependent: :delete_all
-
# end
-
#
-
# person.pets.size # => 3
-
# person.pets
-
# # => [
-
# # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
-
# # #<Pet id: 2, name: "Spook", person_id: 1>,
-
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
-
# # ]
-
#
-
# person.pets.delete(Pet.find(1))
-
# # => [#<Pet id: 1, name: "Fancy-Fancy", person_id: 1>]
-
#
-
# person.pets.size # => 2
-
# person.pets
-
# # => [
-
# # #<Pet id: 2, name: "Spook", person_id: 1>,
-
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
-
# # ]
-
#
-
# Pet.find(1)
-
# # => ActiveRecord::RecordNotFound: Couldn't find Pet with 'id'=1
-
#
-
# You can pass +Integer+ or +String+ values, it finds the records
-
# responding to the +id+ and executes delete on them.
-
#
-
# class Person < ActiveRecord::Base
-
# has_many :pets
-
# end
-
#
-
# person.pets.size # => 3
-
# person.pets
-
# # => [
-
# # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
-
# # #<Pet id: 2, name: "Spook", person_id: 1>,
-
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
-
# # ]
-
#
-
# person.pets.delete("1")
-
# # => [#<Pet id: 1, name: "Fancy-Fancy", person_id: 1>]
-
#
-
# person.pets.delete(2, 3)
-
# # => [
-
# # #<Pet id: 2, name: "Spook", person_id: 1>,
-
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
-
# # ]
-
1
def delete(*records)
-
@association.delete(*records).tap { reset_scope }
-
end
-
-
# Destroys the +records+ supplied and removes them from the collection.
-
# This method will _always_ remove record from the database ignoring
-
# the +:dependent+ option. Returns an array with the removed records.
-
#
-
# class Person < ActiveRecord::Base
-
# has_many :pets
-
# end
-
#
-
# person.pets.size # => 3
-
# person.pets
-
# # => [
-
# # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
-
# # #<Pet id: 2, name: "Spook", person_id: 1>,
-
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
-
# # ]
-
#
-
# person.pets.destroy(Pet.find(1))
-
# # => [#<Pet id: 1, name: "Fancy-Fancy", person_id: 1>]
-
#
-
# person.pets.size # => 2
-
# person.pets
-
# # => [
-
# # #<Pet id: 2, name: "Spook", person_id: 1>,
-
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
-
# # ]
-
#
-
# person.pets.destroy(Pet.find(2), Pet.find(3))
-
# # => [
-
# # #<Pet id: 2, name: "Spook", person_id: 1>,
-
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
-
# # ]
-
#
-
# person.pets.size # => 0
-
# person.pets # => []
-
#
-
# Pet.find(1, 2, 3) # => ActiveRecord::RecordNotFound: Couldn't find all Pets with 'id': (1, 2, 3)
-
#
-
# You can pass +Integer+ or +String+ values, it finds the records
-
# responding to the +id+ and then deletes them from the database.
-
#
-
# person.pets.size # => 3
-
# person.pets
-
# # => [
-
# # #<Pet id: 4, name: "Benny", person_id: 1>,
-
# # #<Pet id: 5, name: "Brain", person_id: 1>,
-
# # #<Pet id: 6, name: "Boss", person_id: 1>
-
# # ]
-
#
-
# person.pets.destroy("4")
-
# # => #<Pet id: 4, name: "Benny", person_id: 1>
-
#
-
# person.pets.size # => 2
-
# person.pets
-
# # => [
-
# # #<Pet id: 5, name: "Brain", person_id: 1>,
-
# # #<Pet id: 6, name: "Boss", person_id: 1>
-
# # ]
-
#
-
# person.pets.destroy(5, 6)
-
# # => [
-
# # #<Pet id: 5, name: "Brain", person_id: 1>,
-
# # #<Pet id: 6, name: "Boss", person_id: 1>
-
# # ]
-
#
-
# person.pets.size # => 0
-
# person.pets # => []
-
#
-
# Pet.find(4, 5, 6) # => ActiveRecord::RecordNotFound: Couldn't find all Pets with 'id': (4, 5, 6)
-
1
def destroy(*records)
-
@association.destroy(*records).tap { reset_scope }
-
end
-
-
##
-
# :method: distinct
-
#
-
# :call-seq:
-
# distinct(value = true)
-
#
-
# Specifies whether the records should be unique or not.
-
#
-
# class Person < ActiveRecord::Base
-
# has_many :pets
-
# end
-
#
-
# person.pets.select(:name)
-
# # => [
-
# # #<Pet name: "Fancy-Fancy">,
-
# # #<Pet name: "Fancy-Fancy">
-
# # ]
-
#
-
# person.pets.select(:name).distinct
-
# # => [#<Pet name: "Fancy-Fancy">]
-
#
-
# person.pets.select(:name).distinct.distinct(false)
-
# # => [
-
# # #<Pet name: "Fancy-Fancy">,
-
# # #<Pet name: "Fancy-Fancy">
-
# # ]
-
-
#--
-
1
def calculate(operation, column_name)
-
null_scope? ? scope.calculate(operation, column_name) : super
-
end
-
-
1
def pluck(*column_names)
-
null_scope? ? scope.pluck(*column_names) : super
-
end
-
-
##
-
# :method: count
-
#
-
# :call-seq:
-
# count(column_name = nil, &block)
-
#
-
# Count all records.
-
#
-
# class Person < ActiveRecord::Base
-
# has_many :pets
-
# end
-
#
-
# # This will perform the count using SQL.
-
# person.pets.count # => 3
-
# person.pets
-
# # => [
-
# # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
-
# # #<Pet id: 2, name: "Spook", person_id: 1>,
-
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
-
# # ]
-
#
-
# Passing a block will select all of a person's pets in SQL and then
-
# perform the count using Ruby.
-
#
-
# person.pets.count { |pet| pet.name.include?('-') } # => 2
-
-
# Returns the size of the collection. If the collection hasn't been loaded,
-
# it executes a <tt>SELECT COUNT(*)</tt> query. Else it calls <tt>collection.size</tt>.
-
#
-
# If the collection has been already loaded +size+ and +length+ are
-
# equivalent. If not and you are going to need the records anyway
-
# +length+ will take one less query. Otherwise +size+ is more efficient.
-
#
-
# class Person < ActiveRecord::Base
-
# has_many :pets
-
# end
-
#
-
# person.pets.size # => 3
-
# # executes something like SELECT COUNT(*) FROM "pets" WHERE "pets"."person_id" = 1
-
#
-
# person.pets # This will execute a SELECT * FROM query
-
# # => [
-
# # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
-
# # #<Pet id: 2, name: "Spook", person_id: 1>,
-
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
-
# # ]
-
#
-
# person.pets.size # => 3
-
# # Because the collection is already loaded, this will behave like
-
# # collection.size and no SQL count query is executed.
-
1
def size
-
@association.size
-
end
-
-
##
-
# :method: length
-
#
-
# :call-seq:
-
# length()
-
#
-
# Returns the size of the collection calling +size+ on the target.
-
# If the collection has been already loaded, +length+ and +size+ are
-
# equivalent. If not and you are going to need the records anyway this
-
# method will take one less query. Otherwise +size+ is more efficient.
-
#
-
# class Person < ActiveRecord::Base
-
# has_many :pets
-
# end
-
#
-
# person.pets.length # => 3
-
# # executes something like SELECT "pets".* FROM "pets" WHERE "pets"."person_id" = 1
-
#
-
# # Because the collection is loaded, you can
-
# # call the collection with no additional queries:
-
# person.pets
-
# # => [
-
# # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
-
# # #<Pet id: 2, name: "Spook", person_id: 1>,
-
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
-
# # ]
-
-
# Returns +true+ if the collection is empty. If the collection has been
-
# loaded it is equivalent
-
# to <tt>collection.size.zero?</tt>. If the collection has not been loaded,
-
# it is equivalent to <tt>!collection.exists?</tt>. If the collection has
-
# not already been loaded and you are going to fetch the records anyway it
-
# is better to check <tt>collection.length.zero?</tt>.
-
#
-
# class Person < ActiveRecord::Base
-
# has_many :pets
-
# end
-
#
-
# person.pets.count # => 1
-
# person.pets.empty? # => false
-
#
-
# person.pets.delete_all
-
#
-
# person.pets.count # => 0
-
# person.pets.empty? # => true
-
1
def empty?
-
@association.empty?
-
end
-
-
##
-
# :method: any?
-
#
-
# :call-seq:
-
# any?()
-
#
-
# Returns +true+ if the collection is not empty.
-
#
-
# class Person < ActiveRecord::Base
-
# has_many :pets
-
# end
-
#
-
# person.pets.count # => 0
-
# person.pets.any? # => false
-
#
-
# person.pets << Pet.new(name: 'Snoop')
-
# person.pets.count # => 1
-
# person.pets.any? # => true
-
#
-
# You can also pass a +block+ to define criteria. The behavior
-
# is the same, it returns true if the collection based on the
-
# criteria is not empty.
-
#
-
# person.pets
-
# # => [#<Pet name: "Snoop", group: "dogs">]
-
#
-
# person.pets.any? do |pet|
-
# pet.group == 'cats'
-
# end
-
# # => false
-
#
-
# person.pets.any? do |pet|
-
# pet.group == 'dogs'
-
# end
-
# # => true
-
-
##
-
# :method: many?
-
#
-
# :call-seq:
-
# many?()
-
#
-
# Returns true if the collection has more than one record.
-
# Equivalent to <tt>collection.size > 1</tt>.
-
#
-
# class Person < ActiveRecord::Base
-
# has_many :pets
-
# end
-
#
-
# person.pets.count # => 1
-
# person.pets.many? # => false
-
#
-
# person.pets << Pet.new(name: 'Snoopy')
-
# person.pets.count # => 2
-
# person.pets.many? # => true
-
#
-
# You can also pass a +block+ to define criteria. The
-
# behavior is the same, it returns true if the collection
-
# based on the criteria has more than one record.
-
#
-
# person.pets
-
# # => [
-
# # #<Pet name: "Gorby", group: "cats">,
-
# # #<Pet name: "Puff", group: "cats">,
-
# # #<Pet name: "Snoop", group: "dogs">
-
# # ]
-
#
-
# person.pets.many? do |pet|
-
# pet.group == 'dogs'
-
# end
-
# # => false
-
#
-
# person.pets.many? do |pet|
-
# pet.group == 'cats'
-
# end
-
# # => true
-
-
# Returns +true+ if the given +record+ is present in the collection.
-
#
-
# class Person < ActiveRecord::Base
-
# has_many :pets
-
# end
-
#
-
# person.pets # => [#<Pet id: 20, name: "Snoop">]
-
#
-
# person.pets.include?(Pet.find(20)) # => true
-
# person.pets.include?(Pet.find(21)) # => false
-
1
def include?(record)
-
!!@association.include?(record)
-
end
-
-
1
def proxy_association
-
@association
-
end
-
-
# Returns a <tt>Relation</tt> object for the records in this association
-
1
def scope
-
@scope ||= @association.scope
-
end
-
-
# Equivalent to <tt>Array#==</tt>. Returns +true+ if the two arrays
-
# contain the same number of elements and if each element is equal
-
# to the corresponding element in the +other+ array, otherwise returns
-
# +false+.
-
#
-
# class Person < ActiveRecord::Base
-
# has_many :pets
-
# end
-
#
-
# person.pets
-
# # => [
-
# # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
-
# # #<Pet id: 2, name: "Spook", person_id: 1>
-
# # ]
-
#
-
# other = person.pets.to_ary
-
#
-
# person.pets == other
-
# # => true
-
#
-
# other = [Pet.new(id: 1), Pet.new(id: 2)]
-
#
-
# person.pets == other
-
# # => false
-
1
def ==(other)
-
load_target == other
-
end
-
-
##
-
# :method: to_ary
-
#
-
# :call-seq:
-
# to_ary()
-
#
-
# Returns a new array of objects from the collection. If the collection
-
# hasn't been loaded, it fetches the records from the database.
-
#
-
# class Person < ActiveRecord::Base
-
# has_many :pets
-
# end
-
#
-
# person.pets
-
# # => [
-
# # #<Pet id: 4, name: "Benny", person_id: 1>,
-
# # #<Pet id: 5, name: "Brain", person_id: 1>,
-
# # #<Pet id: 6, name: "Boss", person_id: 1>
-
# # ]
-
#
-
# other_pets = person.pets.to_ary
-
# # => [
-
# # #<Pet id: 4, name: "Benny", person_id: 1>,
-
# # #<Pet id: 5, name: "Brain", person_id: 1>,
-
# # #<Pet id: 6, name: "Boss", person_id: 1>
-
# # ]
-
#
-
# other_pets.replace([Pet.new(name: 'BooGoo')])
-
#
-
# other_pets
-
# # => [#<Pet id: nil, name: "BooGoo", person_id: 1>]
-
#
-
# person.pets
-
# # This is not affected by replace
-
# # => [
-
# # #<Pet id: 4, name: "Benny", person_id: 1>,
-
# # #<Pet id: 5, name: "Brain", person_id: 1>,
-
# # #<Pet id: 6, name: "Boss", person_id: 1>
-
# # ]
-
-
1
def records # :nodoc:
-
load_target
-
end
-
-
# Adds one or more +records+ to the collection by setting their foreign keys
-
# to the association's primary key. Since +<<+ flattens its argument list and
-
# inserts each record, +push+ and +concat+ behave identically. Returns +self+
-
# so several appends may be chained together.
-
#
-
# class Person < ActiveRecord::Base
-
# has_many :pets
-
# end
-
#
-
# person.pets.size # => 0
-
# person.pets << Pet.new(name: 'Fancy-Fancy')
-
# person.pets << [Pet.new(name: 'Spook'), Pet.new(name: 'Choo-Choo')]
-
# person.pets.size # => 3
-
#
-
# person.id # => 1
-
# person.pets
-
# # => [
-
# # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
-
# # #<Pet id: 2, name: "Spook", person_id: 1>,
-
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
-
# # ]
-
1
def <<(*records)
-
proxy_association.concat(records) && self
-
end
-
1
alias_method :push, :<<
-
1
alias_method :append, :<<
-
1
alias_method :concat, :<<
-
-
1
def prepend(*args)
-
raise NoMethodError, "prepend on association is not defined. Please use <<, push or append"
-
end
-
-
# Equivalent to +delete_all+. The difference is that returns +self+, instead
-
# of an array with the deleted objects, so methods can be chained. See
-
# +delete_all+ for more information.
-
# Note that because +delete_all+ removes records by directly
-
# running an SQL query into the database, the +updated_at+ column of
-
# the object is not changed.
-
1
def clear
-
delete_all
-
self
-
end
-
-
# Reloads the collection from the database. Returns +self+.
-
#
-
# class Person < ActiveRecord::Base
-
# has_many :pets
-
# end
-
#
-
# person.pets # fetches pets from the database
-
# # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>]
-
#
-
# person.pets # uses the pets cache
-
# # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>]
-
#
-
# person.pets.reload # fetches pets from the database
-
# # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>]
-
1
def reload
-
proxy_association.reload
-
reset_scope
-
end
-
-
# Unloads the association. Returns +self+.
-
#
-
# class Person < ActiveRecord::Base
-
# has_many :pets
-
# end
-
#
-
# person.pets # fetches pets from the database
-
# # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>]
-
#
-
# person.pets # uses the pets cache
-
# # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>]
-
#
-
# person.pets.reset # clears the pets cache
-
#
-
# person.pets # fetches pets from the database
-
# # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>]
-
1
def reset
-
proxy_association.reset
-
proxy_association.reset_scope
-
reset_scope
-
end
-
-
1
def reset_scope # :nodoc:
-
@offsets = {}
-
@scope = nil
-
self
-
end
-
-
delegate_methods = [
-
1
QueryMethods,
-
SpawnMethods,
-
].flat_map { |klass|
-
2
klass.public_instance_methods(false)
-
} - self.public_instance_methods(false) - [:select] + [:scoping]
-
-
1
delegate(*delegate_methods, to: :scope)
-
-
1
private
-
-
1
def find_nth_with_limit(index, limit)
-
load_target if find_from_target?
-
super
-
end
-
-
1
def find_nth_from_last(index)
-
load_target if find_from_target?
-
super
-
end
-
-
1
def null_scope?
-
@association.null_scope?
-
end
-
-
1
def find_from_target?
-
@association.find_from_target?
-
end
-
-
1
def exec_queries
-
load_target
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActiveRecord
-
1
module ConnectionAdapters
-
1
class TransactionState
-
1
def initialize(state = nil)
-
6
@state = state
-
6
@children = []
-
end
-
-
1
def add_child(state)
-
@children << state
-
end
-
-
1
def finalized?
-
@state
-
end
-
-
1
def committed?
-
@state == :committed || @state == :fully_committed
-
end
-
-
1
def fully_committed?
-
@state == :fully_committed
-
end
-
-
1
def rolledback?
-
@state == :rolledback || @state == :fully_rolledback
-
end
-
-
1
def fully_rolledback?
-
@state == :fully_rolledback
-
end
-
-
1
def fully_completed?
-
completed?
-
end
-
-
1
def completed?
-
committed? || rolledback?
-
end
-
-
1
def set_state(state)
-
ActiveSupport::Deprecation.warn(<<-MSG.squish)
-
The set_state method is deprecated and will be removed in
-
Rails 6.0. Please use rollback! or commit! to set transaction
-
state directly.
-
MSG
-
case state
-
when :rolledback
-
rollback!
-
when :committed
-
commit!
-
when nil
-
nullify!
-
else
-
raise ArgumentError, "Invalid transaction state: #{state}"
-
end
-
end
-
-
1
def rollback!
-
@children.each { |c| c.rollback! }
-
@state = :rolledback
-
end
-
-
1
def full_rollback!
-
6
@children.each { |c| c.rollback! }
-
6
@state = :fully_rolledback
-
end
-
-
1
def commit!
-
@state = :committed
-
end
-
-
1
def full_commit!
-
@state = :fully_committed
-
end
-
-
1
def nullify!
-
@state = nil
-
end
-
end
-
-
1
class NullTransaction #:nodoc:
-
1
def initialize; end
-
1
def state; end
-
1
def closed?; true; end
-
1
def open?; false; end
-
7
def joinable?; false; end
-
1
def add_record(record); end
-
end
-
-
1
class Transaction #:nodoc:
-
1
attr_reader :connection, :state, :records, :savepoint_name
-
1
attr_writer :joinable
-
-
1
def initialize(connection, options, run_commit_callbacks: false)
-
6
@connection = connection
-
6
@state = TransactionState.new
-
6
@records = []
-
6
@joinable = options.fetch(:joinable, true)
-
6
@run_commit_callbacks = run_commit_callbacks
-
end
-
-
1
def add_record(record)
-
records << record
-
end
-
-
1
def rollback_records
-
6
ite = records.uniq
-
6
while record = ite.shift
-
record.rolledback!(force_restore_state: full_rollback?)
-
end
-
ensure
-
6
ite.each do |i|
-
i.rolledback!(force_restore_state: full_rollback?, should_run_callbacks: false)
-
end
-
end
-
-
1
def before_commit_records
-
records.uniq.each(&:before_committed!) if @run_commit_callbacks
-
end
-
-
1
def commit_records
-
ite = records.uniq
-
while record = ite.shift
-
if @run_commit_callbacks
-
record.committed!
-
else
-
# if not running callbacks, only adds the record to the parent transaction
-
record.add_to_transaction
-
end
-
end
-
ensure
-
ite.each { |i| i.committed!(should_run_callbacks: false) }
-
end
-
-
1
def full_rollback?; true; end
-
1
def joinable?; @joinable; end
-
9
def closed?; false; end
-
9
def open?; !closed?; end
-
end
-
-
1
class SavepointTransaction < Transaction
-
1
def initialize(connection, savepoint_name, parent_transaction, options, *args)
-
super(connection, options, *args)
-
-
parent_transaction.state.add_child(@state)
-
-
if options[:isolation]
-
raise ActiveRecord::TransactionIsolationError, "cannot set transaction isolation in a nested transaction"
-
end
-
connection.create_savepoint(@savepoint_name = savepoint_name)
-
end
-
-
1
def rollback
-
connection.rollback_to_savepoint(savepoint_name)
-
@state.rollback!
-
end
-
-
1
def commit
-
connection.release_savepoint(savepoint_name)
-
@state.commit!
-
end
-
-
1
def full_rollback?; false; end
-
end
-
-
1
class RealTransaction < Transaction
-
1
def initialize(connection, options, *args)
-
6
super
-
6
if options[:isolation]
-
connection.begin_isolated_db_transaction(options[:isolation])
-
else
-
6
connection.begin_db_transaction
-
end
-
end
-
-
1
def rollback
-
6
connection.rollback_db_transaction
-
6
@state.full_rollback!
-
end
-
-
1
def commit
-
connection.commit_db_transaction
-
@state.full_commit!
-
end
-
end
-
-
1
class TransactionManager #:nodoc:
-
1
def initialize(connection)
-
3
@stack = []
-
3
@connection = connection
-
end
-
-
1
def begin_transaction(options = {})
-
6
@connection.lock.synchronize do
-
6
run_commit_callbacks = !current_transaction.joinable?
-
transaction =
-
5
if @stack.empty?
-
6
RealTransaction.new(@connection, options, run_commit_callbacks: run_commit_callbacks)
-
else
-
SavepointTransaction.new(@connection, "active_record_#{@stack.size}", @stack.last, options,
-
run_commit_callbacks: run_commit_callbacks)
-
end
-
-
6
@stack.push(transaction)
-
6
transaction
-
end
-
end
-
-
1
def commit_transaction
-
@connection.lock.synchronize do
-
transaction = @stack.last
-
-
begin
-
transaction.before_commit_records
-
ensure
-
@stack.pop
-
end
-
-
transaction.commit
-
transaction.commit_records
-
end
-
end
-
-
1
def rollback_transaction(transaction = nil)
-
6
@connection.lock.synchronize do
-
6
transaction ||= @stack.pop
-
6
transaction.rollback
-
6
transaction.rollback_records
-
end
-
end
-
-
1
def within_new_transaction(options = {})
-
@connection.lock.synchronize do
-
begin
-
transaction = begin_transaction options
-
yield
-
rescue Exception => error
-
if transaction
-
rollback_transaction
-
after_failure_actions(transaction, error)
-
end
-
raise
-
ensure
-
unless error
-
if Thread.current.status == "aborting"
-
rollback_transaction if transaction
-
else
-
begin
-
commit_transaction if transaction
-
rescue Exception
-
rollback_transaction(transaction) unless transaction.state.completed?
-
raise
-
end
-
end
-
end
-
end
-
end
-
end
-
-
1
def open_transactions
-
@stack.size
-
end
-
-
1
def current_transaction
-
14
@stack.last || NULL_TRANSACTION
-
end
-
-
1
private
-
-
1
NULL_TRANSACTION = NullTransaction.new
-
-
# Deallocate invalidated prepared statements outside of the transaction
-
1
def after_failure_actions(transaction, error)
-
return unless transaction.is_a?(RealTransaction)
-
return unless error.is_a?(ActiveRecord::PreparedStatementCacheExpired)
-
@connection.clear_cache!
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "erb"
-
1
require "yaml"
-
-
1
module ActiveRecord
-
1
class FixtureSet
-
1
class File # :nodoc:
-
1
include Enumerable
-
-
##
-
# Open a fixture file named +file+. When called with a block, the block
-
# is called with the filehandle and the filehandle is automatically closed
-
# when the block finishes.
-
1
def self.open(file)
-
x = new file
-
block_given? ? yield(x) : x
-
end
-
-
1
def initialize(file)
-
@file = file
-
end
-
-
1
def each(&block)
-
rows.each(&block)
-
end
-
-
1
def model_class
-
config_row["model_class"]
-
end
-
-
1
private
-
1
def rows
-
@rows ||= raw_rows.reject { |fixture_name, _| fixture_name == "_fixture" }
-
end
-
-
1
def config_row
-
@config_row ||= begin
-
row = raw_rows.find { |fixture_name, _| fixture_name == "_fixture" }
-
if row
-
row.last
-
else
-
{ 'model_class': nil }
-
end
-
end
-
end
-
-
1
def raw_rows
-
@raw_rows ||= begin
-
data = YAML.load(render(IO.read(@file)))
-
data ? validate(data).to_a : []
-
rescue ArgumentError, Psych::SyntaxError => error
-
raise Fixture::FormatError, "a YAML error occurred parsing #{@file}. Please note that YAML must be consistently indented using spaces. Tabs are not allowed. Please have a look at http://www.yaml.org/faq.html\nThe exact error was:\n #{error.class}: #{error}", error.backtrace
-
end
-
end
-
-
1
def prepare_erb(content)
-
erb = ERB.new(content)
-
erb.filename = @file
-
erb
-
end
-
-
1
def render(content)
-
context = ActiveRecord::FixtureSet::RenderContext.create_subclass.new
-
prepare_erb(content).result(context.get_binding)
-
end
-
-
# Validate our unmarshalled data.
-
1
def validate(data)
-
unless Hash === data || YAML::Omap === data
-
raise Fixture::FormatError, "fixture is not a hash: #{@file}"
-
end
-
-
invalid = data.reject { |_, row| Hash === row }
-
if invalid.any?
-
raise Fixture::FormatError, "fixture key is not a hash: #{@file}, keys: #{invalid.keys.inspect}"
-
end
-
data
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "erb"
-
1
require "yaml"
-
1
require "zlib"
-
1
require "set"
-
1
require "active_support/dependencies"
-
1
require "active_support/core_ext/digest/uuid"
-
1
require "active_record/fixture_set/file"
-
1
require "active_record/errors"
-
-
1
module ActiveRecord
-
1
class FixtureClassNotFound < ActiveRecord::ActiveRecordError #:nodoc:
-
end
-
-
# \Fixtures are a way of organizing data that you want to test against; in short, sample data.
-
#
-
# They are stored in YAML files, one file per model, which are placed in the directory
-
# appointed by <tt>ActiveSupport::TestCase.fixture_path=(path)</tt> (this is automatically
-
# configured for Rails, so you can just put your files in <tt><your-rails-app>/test/fixtures/</tt>).
-
# The fixture file ends with the +.yml+ file extension, for example:
-
# <tt><your-rails-app>/test/fixtures/web_sites.yml</tt>).
-
#
-
# The format of a fixture file looks like this:
-
#
-
# rubyonrails:
-
# id: 1
-
# name: Ruby on Rails
-
# url: http://www.rubyonrails.org
-
#
-
# google:
-
# id: 2
-
# name: Google
-
# url: http://www.google.com
-
#
-
# This fixture file includes two fixtures. Each YAML fixture (ie. record) is given a name and
-
# is followed by an indented list of key/value pairs in the "key: value" format. Records are
-
# separated by a blank line for your viewing pleasure.
-
#
-
# Note: Fixtures are unordered. If you want ordered fixtures, use the omap YAML type.
-
# See http://yaml.org/type/omap.html
-
# for the specification. You will need ordered fixtures when you have foreign key constraints
-
# on keys in the same table. This is commonly needed for tree structures. Example:
-
#
-
# --- !omap
-
# - parent:
-
# id: 1
-
# parent_id: NULL
-
# title: Parent
-
# - child:
-
# id: 2
-
# parent_id: 1
-
# title: Child
-
#
-
# = Using Fixtures in Test Cases
-
#
-
# Since fixtures are a testing construct, we use them in our unit and functional tests. There
-
# are two ways to use the fixtures, but first let's take a look at a sample unit test:
-
#
-
# require 'test_helper'
-
#
-
# class WebSiteTest < ActiveSupport::TestCase
-
# test "web_site_count" do
-
# assert_equal 2, WebSite.count
-
# end
-
# end
-
#
-
# By default, +test_helper.rb+ will load all of your fixtures into your test
-
# database, so this test will succeed.
-
#
-
# The testing environment will automatically load all the fixtures into the database before each
-
# test. To ensure consistent data, the environment deletes the fixtures before running the load.
-
#
-
# In addition to being available in the database, the fixture's data may also be accessed by
-
# using a special dynamic method, which has the same name as the model.
-
#
-
# Passing in a fixture name to this dynamic method returns the fixture matching this name:
-
#
-
# test "find one" do
-
# assert_equal "Ruby on Rails", web_sites(:rubyonrails).name
-
# end
-
#
-
# Passing in multiple fixture names returns all fixtures matching these names:
-
#
-
# test "find all by name" do
-
# assert_equal 2, web_sites(:rubyonrails, :google).length
-
# end
-
#
-
# Passing in no arguments returns all fixtures:
-
#
-
# test "find all" do
-
# assert_equal 2, web_sites.length
-
# end
-
#
-
# Passing in any fixture name that does not exist will raise <tt>StandardError</tt>:
-
#
-
# test "find by name that does not exist" do
-
# assert_raise(StandardError) { web_sites(:reddit) }
-
# end
-
#
-
# Alternatively, you may enable auto-instantiation of the fixture data. For instance, take the
-
# following tests:
-
#
-
# test "find_alt_method_1" do
-
# assert_equal "Ruby on Rails", @web_sites['rubyonrails']['name']
-
# end
-
#
-
# test "find_alt_method_2" do
-
# assert_equal "Ruby on Rails", @rubyonrails.name
-
# end
-
#
-
# In order to use these methods to access fixtured data within your test cases, you must specify one of the
-
# following in your ActiveSupport::TestCase-derived class:
-
#
-
# - to fully enable instantiated fixtures (enable alternate methods #1 and #2 above)
-
# self.use_instantiated_fixtures = true
-
#
-
# - create only the hash for the fixtures, do not 'find' each instance (enable alternate method #1 only)
-
# self.use_instantiated_fixtures = :no_instances
-
#
-
# Using either of these alternate methods incurs a performance hit, as the fixtured data must be fully
-
# traversed in the database to create the fixture hash and/or instance variables. This is expensive for
-
# large sets of fixtured data.
-
#
-
# = Dynamic fixtures with ERB
-
#
-
# Sometimes you don't care about the content of the fixtures as much as you care about the volume.
-
# In these cases, you can mix ERB in with your YAML fixtures to create a bunch of fixtures for load
-
# testing, like:
-
#
-
# <% 1.upto(1000) do |i| %>
-
# fix_<%= i %>:
-
# id: <%= i %>
-
# name: guy_<%= i %>
-
# <% end %>
-
#
-
# This will create 1000 very simple fixtures.
-
#
-
# Using ERB, you can also inject dynamic values into your fixtures with inserts like
-
# <tt><%= Date.today.strftime("%Y-%m-%d") %></tt>.
-
# This is however a feature to be used with some caution. The point of fixtures are that they're
-
# stable units of predictable sample data. If you feel that you need to inject dynamic values, then
-
# perhaps you should reexamine whether your application is properly testable. Hence, dynamic values
-
# in fixtures are to be considered a code smell.
-
#
-
# Helper methods defined in a fixture will not be available in other fixtures, to prevent against
-
# unwanted inter-test dependencies. Methods used by multiple fixtures should be defined in a module
-
# that is included in ActiveRecord::FixtureSet.context_class.
-
#
-
# - define a helper method in <tt>test_helper.rb</tt>
-
# module FixtureFileHelpers
-
# def file_sha(path)
-
# Digest::SHA2.hexdigest(File.read(Rails.root.join('test/fixtures', path)))
-
# end
-
# end
-
# ActiveRecord::FixtureSet.context_class.include FixtureFileHelpers
-
#
-
# - use the helper method in a fixture
-
# photo:
-
# name: kitten.png
-
# sha: <%= file_sha 'files/kitten.png' %>
-
#
-
# = Transactional Tests
-
#
-
# Test cases can use begin+rollback to isolate their changes to the database instead of having to
-
# delete+insert for every test case.
-
#
-
# class FooTest < ActiveSupport::TestCase
-
# self.use_transactional_tests = true
-
#
-
# test "godzilla" do
-
# assert_not_empty Foo.all
-
# Foo.destroy_all
-
# assert_empty Foo.all
-
# end
-
#
-
# test "godzilla aftermath" do
-
# assert_not_empty Foo.all
-
# end
-
# end
-
#
-
# If you preload your test database with all fixture data (probably in the rake task) and use
-
# transactional tests, then you may omit all fixtures declarations in your test cases since
-
# all the data's already there and every case rolls back its changes.
-
#
-
# In order to use instantiated fixtures with preloaded data, set +self.pre_loaded_fixtures+ to
-
# true. This will provide access to fixture data for every table that has been loaded through
-
# fixtures (depending on the value of +use_instantiated_fixtures+).
-
#
-
# When *not* to use transactional tests:
-
#
-
# 1. You're testing whether a transaction works correctly. Nested transactions don't commit until
-
# all parent transactions commit, particularly, the fixtures transaction which is begun in setup
-
# and rolled back in teardown. Thus, you won't be able to verify
-
# the results of your transaction until Active Record supports nested transactions or savepoints (in progress).
-
# 2. Your database does not support transactions. Every Active Record database supports transactions except MySQL MyISAM.
-
# Use InnoDB, MaxDB, or NDB instead.
-
#
-
# = Advanced Fixtures
-
#
-
# Fixtures that don't specify an ID get some extra features:
-
#
-
# * Stable, autogenerated IDs
-
# * Label references for associations (belongs_to, has_one, has_many)
-
# * HABTM associations as inline lists
-
#
-
# There are some more advanced features available even if the id is specified:
-
#
-
# * Autofilled timestamp columns
-
# * Fixture label interpolation
-
# * Support for YAML defaults
-
#
-
# == Stable, Autogenerated IDs
-
#
-
# Here, have a monkey fixture:
-
#
-
# george:
-
# id: 1
-
# name: George the Monkey
-
#
-
# reginald:
-
# id: 2
-
# name: Reginald the Pirate
-
#
-
# Each of these fixtures has two unique identifiers: one for the database
-
# and one for the humans. Why don't we generate the primary key instead?
-
# Hashing each fixture's label yields a consistent ID:
-
#
-
# george: # generated id: 503576764
-
# name: George the Monkey
-
#
-
# reginald: # generated id: 324201669
-
# name: Reginald the Pirate
-
#
-
# Active Record looks at the fixture's model class, discovers the correct
-
# primary key, and generates it right before inserting the fixture
-
# into the database.
-
#
-
# The generated ID for a given label is constant, so we can discover
-
# any fixture's ID without loading anything, as long as we know the label.
-
#
-
# == Label references for associations (belongs_to, has_one, has_many)
-
#
-
# Specifying foreign keys in fixtures can be very fragile, not to
-
# mention difficult to read. Since Active Record can figure out the ID of
-
# any fixture from its label, you can specify FK's by label instead of ID.
-
#
-
# === belongs_to
-
#
-
# Let's break out some more monkeys and pirates.
-
#
-
# ### in pirates.yml
-
#
-
# reginald:
-
# id: 1
-
# name: Reginald the Pirate
-
# monkey_id: 1
-
#
-
# ### in monkeys.yml
-
#
-
# george:
-
# id: 1
-
# name: George the Monkey
-
# pirate_id: 1
-
#
-
# Add a few more monkeys and pirates and break this into multiple files,
-
# and it gets pretty hard to keep track of what's going on. Let's
-
# use labels instead of IDs:
-
#
-
# ### in pirates.yml
-
#
-
# reginald:
-
# name: Reginald the Pirate
-
# monkey: george
-
#
-
# ### in monkeys.yml
-
#
-
# george:
-
# name: George the Monkey
-
# pirate: reginald
-
#
-
# Pow! All is made clear. Active Record reflects on the fixture's model class,
-
# finds all the +belongs_to+ associations, and allows you to specify
-
# a target *label* for the *association* (monkey: george) rather than
-
# a target *id* for the *FK* (<tt>monkey_id: 1</tt>).
-
#
-
# ==== Polymorphic belongs_to
-
#
-
# Supporting polymorphic relationships is a little bit more complicated, since
-
# Active Record needs to know what type your association is pointing at. Something
-
# like this should look familiar:
-
#
-
# ### in fruit.rb
-
#
-
# belongs_to :eater, polymorphic: true
-
#
-
# ### in fruits.yml
-
#
-
# apple:
-
# id: 1
-
# name: apple
-
# eater_id: 1
-
# eater_type: Monkey
-
#
-
# Can we do better? You bet!
-
#
-
# apple:
-
# eater: george (Monkey)
-
#
-
# Just provide the polymorphic target type and Active Record will take care of the rest.
-
#
-
# === has_and_belongs_to_many
-
#
-
# Time to give our monkey some fruit.
-
#
-
# ### in monkeys.yml
-
#
-
# george:
-
# id: 1
-
# name: George the Monkey
-
#
-
# ### in fruits.yml
-
#
-
# apple:
-
# id: 1
-
# name: apple
-
#
-
# orange:
-
# id: 2
-
# name: orange
-
#
-
# grape:
-
# id: 3
-
# name: grape
-
#
-
# ### in fruits_monkeys.yml
-
#
-
# apple_george:
-
# fruit_id: 1
-
# monkey_id: 1
-
#
-
# orange_george:
-
# fruit_id: 2
-
# monkey_id: 1
-
#
-
# grape_george:
-
# fruit_id: 3
-
# monkey_id: 1
-
#
-
# Let's make the HABTM fixture go away.
-
#
-
# ### in monkeys.yml
-
#
-
# george:
-
# id: 1
-
# name: George the Monkey
-
# fruits: apple, orange, grape
-
#
-
# ### in fruits.yml
-
#
-
# apple:
-
# name: apple
-
#
-
# orange:
-
# name: orange
-
#
-
# grape:
-
# name: grape
-
#
-
# Zap! No more fruits_monkeys.yml file. We've specified the list of fruits
-
# on George's fixture, but we could've just as easily specified a list
-
# of monkeys on each fruit. As with +belongs_to+, Active Record reflects on
-
# the fixture's model class and discovers the +has_and_belongs_to_many+
-
# associations.
-
#
-
# == Autofilled Timestamp Columns
-
#
-
# If your table/model specifies any of Active Record's
-
# standard timestamp columns (+created_at+, +created_on+, +updated_at+, +updated_on+),
-
# they will automatically be set to <tt>Time.now</tt>.
-
#
-
# If you've set specific values, they'll be left alone.
-
#
-
# == Fixture label interpolation
-
#
-
# The label of the current fixture is always available as a column value:
-
#
-
# geeksomnia:
-
# name: Geeksomnia's Account
-
# subdomain: $LABEL
-
# email: $LABEL@email.com
-
#
-
# Also, sometimes (like when porting older join table fixtures) you'll need
-
# to be able to get a hold of the identifier for a given label. ERB
-
# to the rescue:
-
#
-
# george_reginald:
-
# monkey_id: <%= ActiveRecord::FixtureSet.identify(:reginald) %>
-
# pirate_id: <%= ActiveRecord::FixtureSet.identify(:george) %>
-
#
-
# == Support for YAML defaults
-
#
-
# You can set and reuse defaults in your fixtures YAML file.
-
# This is the same technique used in the +database.yml+ file to specify
-
# defaults:
-
#
-
# DEFAULTS: &DEFAULTS
-
# created_on: <%= 3.weeks.ago.to_s(:db) %>
-
#
-
# first:
-
# name: Smurf
-
# <<: *DEFAULTS
-
#
-
# second:
-
# name: Fraggle
-
# <<: *DEFAULTS
-
#
-
# Any fixture labeled "DEFAULTS" is safely ignored.
-
#
-
# == Configure the fixture model class
-
#
-
# It's possible to set the fixture's model class directly in the YAML file.
-
# This is helpful when fixtures are loaded outside tests and
-
# +set_fixture_class+ is not available (e.g.
-
# when running <tt>rails db:fixtures:load</tt>).
-
#
-
# _fixture:
-
# model_class: User
-
# david:
-
# name: David
-
#
-
# Any fixtures labeled "_fixture" are safely ignored.
-
1
class FixtureSet
-
#--
-
# An instance of FixtureSet is normally stored in a single YAML file and
-
# possibly in a folder with the same name.
-
#++
-
-
1
MAX_ID = 2**30 - 1
-
-
2
@@all_cached_fixtures = Hash.new { |h, k| h[k] = {} }
-
-
1
def self.default_fixture_model_name(fixture_set_name, config = ActiveRecord::Base) # :nodoc:
-
config.pluralize_table_names ?
-
fixture_set_name.singularize.camelize :
-
fixture_set_name.camelize
-
end
-
-
1
def self.default_fixture_table_name(fixture_set_name, config = ActiveRecord::Base) # :nodoc:
-
"#{ config.table_name_prefix }"\
-
"#{ fixture_set_name.tr('/', '_') }"\
-
"#{ config.table_name_suffix }".to_sym
-
end
-
-
1
def self.reset_cache
-
@@all_cached_fixtures.clear
-
end
-
-
1
def self.cache_for_connection(connection)
-
4
@@all_cached_fixtures[connection]
-
end
-
-
1
def self.fixture_is_cached?(connection, table_name)
-
cache_for_connection(connection)[table_name]
-
end
-
-
1
def self.cached_fixtures(connection, keys_to_fetch = nil)
-
4
if keys_to_fetch
-
4
cache_for_connection(connection).values_at(*keys_to_fetch)
-
else
-
cache_for_connection(connection).values
-
end
-
end
-
-
1
def self.cache_fixtures(connection, fixtures_map)
-
cache_for_connection(connection).update(fixtures_map)
-
end
-
-
1
def self.instantiate_fixtures(object, fixture_set, load_instances = true)
-
if load_instances
-
fixture_set.each do |fixture_name, fixture|
-
begin
-
object.instance_variable_set "@#{fixture_name}", fixture.find
-
rescue FixtureClassNotFound
-
nil
-
end
-
end
-
end
-
end
-
-
1
def self.instantiate_all_loaded_fixtures(object, load_instances = true)
-
all_loaded_fixtures.each_value do |fixture_set|
-
instantiate_fixtures(object, fixture_set, load_instances)
-
end
-
end
-
-
1
cattr_accessor :all_loaded_fixtures, default: {}
-
-
1
class ClassCache
-
1
def initialize(class_names, config)
-
4
@class_names = class_names.stringify_keys
-
4
@config = config
-
-
# Remove string values that aren't constants or subclasses of AR
-
4
@class_names.delete_if { |klass_name, klass| !insert_class(@class_names, klass_name, klass) }
-
end
-
-
1
def [](fs_name)
-
@class_names.fetch(fs_name) {
-
klass = default_fixture_model(fs_name, @config).safe_constantize
-
insert_class(@class_names, fs_name, klass)
-
}
-
end
-
-
1
private
-
-
1
def insert_class(class_names, name, klass)
-
# We only want to deal with AR objects.
-
if klass && klass < ActiveRecord::Base
-
class_names[name] = klass
-
else
-
class_names[name] = nil
-
end
-
end
-
-
1
def default_fixture_model(fs_name, config)
-
ActiveRecord::FixtureSet.default_fixture_model_name(fs_name, config)
-
end
-
end
-
-
1
def self.create_fixtures(fixtures_directory, fixture_set_names, class_names = {}, config = ActiveRecord::Base)
-
4
fixture_set_names = Array(fixture_set_names).map(&:to_s)
-
4
class_names = ClassCache.new class_names, config
-
-
# FIXME: Apparently JK uses this.
-
4
connection = block_given? ? yield : ActiveRecord::Base.connection
-
-
4
files_to_read = fixture_set_names.reject { |fs_name|
-
fixture_is_cached?(connection, fs_name)
-
}
-
-
4
unless files_to_read.empty?
-
fixtures_map = {}
-
-
fixture_sets = files_to_read.map do |fs_name|
-
klass = class_names[fs_name]
-
conn = klass ? klass.connection : connection
-
fixtures_map[fs_name] = new( # ActiveRecord::FixtureSet.new
-
conn,
-
fs_name,
-
klass,
-
::File.join(fixtures_directory, fs_name))
-
end
-
-
update_all_loaded_fixtures fixtures_map
-
fixture_sets_by_connection = fixture_sets.group_by { |fs| fs.model_class ? fs.model_class.connection : connection }
-
-
fixture_sets_by_connection.each do |conn, set|
-
table_rows_for_connection = Hash.new { |h, k| h[k] = [] }
-
-
set.each do |fs|
-
fs.table_rows.each do |table, rows|
-
table_rows_for_connection[table].unshift(*rows)
-
end
-
end
-
conn.insert_fixtures_set(table_rows_for_connection, table_rows_for_connection.keys)
-
-
# Cap primary key sequences to max(pk).
-
if conn.respond_to?(:reset_pk_sequence!)
-
set.each { |fs| conn.reset_pk_sequence!(fs.table_name) }
-
end
-
end
-
-
cache_fixtures(connection, fixtures_map)
-
end
-
4
cached_fixtures(connection, fixture_set_names)
-
end
-
-
# Returns a consistent, platform-independent identifier for +label+.
-
# Integer identifiers are values less than 2^30. UUIDs are RFC 4122 version 5 SHA-1 hashes.
-
1
def self.identify(label, column_type = :integer)
-
if column_type == :uuid
-
Digest::UUID.uuid_v5(Digest::UUID::OID_NAMESPACE, label.to_s)
-
else
-
Zlib.crc32(label.to_s) % MAX_ID
-
end
-
end
-
-
# Superclass for the evaluation contexts used by ERB fixtures.
-
1
def self.context_class
-
@context_class ||= Class.new
-
end
-
-
1
def self.update_all_loaded_fixtures(fixtures_map) # :nodoc:
-
all_loaded_fixtures.update(fixtures_map)
-
end
-
-
1
attr_reader :table_name, :name, :fixtures, :model_class, :config
-
-
1
def initialize(connection, name, class_name, path, config = ActiveRecord::Base)
-
@name = name
-
@path = path
-
@config = config
-
-
self.model_class = class_name
-
-
@fixtures = read_fixture_files(path)
-
-
@connection = connection
-
-
@table_name = (model_class.respond_to?(:table_name) ?
-
model_class.table_name :
-
self.class.default_fixture_table_name(name, config))
-
end
-
-
1
def [](x)
-
fixtures[x]
-
end
-
-
1
def []=(k, v)
-
fixtures[k] = v
-
end
-
-
1
def each(&block)
-
fixtures.each(&block)
-
end
-
-
1
def size
-
fixtures.size
-
end
-
-
# Returns a hash of rows to be inserted. The key is the table, the value is
-
# a list of rows to insert to that table.
-
1
def table_rows
-
now = config.default_timezone == :utc ? Time.now.utc : Time.now
-
-
# allow a standard key to be used for doing defaults in YAML
-
fixtures.delete("DEFAULTS")
-
-
# track any join tables we need to insert later
-
rows = Hash.new { |h, table| h[table] = [] }
-
-
rows[table_name] = fixtures.map do |label, fixture|
-
row = fixture.to_hash
-
-
if model_class
-
# fill in timestamp columns if they aren't specified and the model is set to record_timestamps
-
if model_class.record_timestamps
-
timestamp_column_names.each do |c_name|
-
row[c_name] = now unless row.key?(c_name)
-
end
-
end
-
-
# interpolate the fixture label
-
row.each do |key, value|
-
row[key] = value.gsub("$LABEL", label.to_s) if value.is_a?(String)
-
end
-
-
# generate a primary key if necessary
-
if has_primary_key_column? && !row.include?(primary_key_name)
-
row[primary_key_name] = ActiveRecord::FixtureSet.identify(label, primary_key_type)
-
end
-
-
# Resolve enums
-
model_class.defined_enums.each do |name, values|
-
if row.include?(name)
-
row[name] = values.fetch(row[name], row[name])
-
end
-
end
-
-
# If STI is used, find the correct subclass for association reflection
-
reflection_class =
-
if row.include?(inheritance_column_name)
-
row[inheritance_column_name].constantize rescue model_class
-
else
-
model_class
-
end
-
-
reflection_class._reflections.each_value do |association|
-
case association.macro
-
when :belongs_to
-
# Do not replace association name with association foreign key if they are named the same
-
fk_name = (association.options[:foreign_key] || "#{association.name}_id").to_s
-
-
if association.name.to_s != fk_name && value = row.delete(association.name.to_s)
-
if association.polymorphic? && value.sub!(/\s*\(([^\)]*)\)\s*$/, "")
-
# support polymorphic belongs_to as "label (Type)"
-
row[association.foreign_type] = $1
-
end
-
-
fk_type = reflection_class.type_for_attribute(fk_name).type
-
row[fk_name] = ActiveRecord::FixtureSet.identify(value, fk_type)
-
end
-
when :has_many
-
if association.options[:through]
-
add_join_records(rows, row, HasManyThroughProxy.new(association))
-
end
-
end
-
end
-
end
-
-
row
-
end
-
rows
-
end
-
-
1
class ReflectionProxy # :nodoc:
-
1
def initialize(association)
-
@association = association
-
end
-
-
1
def join_table
-
@association.join_table
-
end
-
-
1
def name
-
@association.name
-
end
-
-
1
def primary_key_type
-
@association.klass.type_for_attribute(@association.klass.primary_key).type
-
end
-
end
-
-
1
class HasManyThroughProxy < ReflectionProxy # :nodoc:
-
1
def rhs_key
-
@association.foreign_key
-
end
-
-
1
def lhs_key
-
@association.through_reflection.foreign_key
-
end
-
-
1
def join_table
-
@association.through_reflection.table_name
-
end
-
end
-
-
1
private
-
1
def primary_key_name
-
@primary_key_name ||= model_class && model_class.primary_key
-
end
-
-
1
def primary_key_type
-
@primary_key_type ||= model_class && model_class.type_for_attribute(model_class.primary_key).type
-
end
-
-
1
def add_join_records(rows, row, association)
-
# This is the case when the join table has no fixtures file
-
if (targets = row.delete(association.name.to_s))
-
table_name = association.join_table
-
column_type = association.primary_key_type
-
lhs_key = association.lhs_key
-
rhs_key = association.rhs_key
-
-
targets = targets.is_a?(Array) ? targets : targets.split(/\s*,\s*/)
-
rows[table_name].concat targets.map { |target|
-
{ lhs_key => row[primary_key_name],
-
rhs_key => ActiveRecord::FixtureSet.identify(target, column_type) }
-
}
-
end
-
end
-
-
1
def has_primary_key_column?
-
@has_primary_key_column ||= primary_key_name &&
-
model_class.columns.any? { |c| c.name == primary_key_name }
-
end
-
-
1
def timestamp_column_names
-
@timestamp_column_names ||=
-
%w(created_at created_on updated_at updated_on) & column_names
-
end
-
-
1
def inheritance_column_name
-
@inheritance_column_name ||= model_class && model_class.inheritance_column
-
end
-
-
1
def column_names
-
@column_names ||= @connection.columns(@table_name).collect(&:name)
-
end
-
-
1
def model_class=(class_name)
-
if class_name.is_a?(Class) # TODO: Should be an AR::Base type class, or any?
-
@model_class = class_name
-
else
-
@model_class = class_name.safe_constantize if class_name
-
end
-
end
-
-
# Loads the fixtures from the YAML file at +path+.
-
# If the file sets the +model_class+ and current instance value is not set,
-
# it uses the file value.
-
1
def read_fixture_files(path)
-
yaml_files = Dir["#{path}/{**,*}/*.yml"].select { |f|
-
::File.file?(f)
-
} + [yaml_file_path(path)]
-
-
yaml_files.each_with_object({}) do |file, fixtures|
-
FixtureSet::File.open(file) do |fh|
-
self.model_class ||= fh.model_class if fh.model_class
-
fh.each do |fixture_name, row|
-
fixtures[fixture_name] = ActiveRecord::Fixture.new(row, model_class)
-
end
-
end
-
end
-
end
-
-
1
def yaml_file_path(path)
-
"#{path}.yml"
-
end
-
end
-
-
1
class Fixture #:nodoc:
-
1
include Enumerable
-
-
1
class FixtureError < StandardError #:nodoc:
-
end
-
-
1
class FormatError < FixtureError #:nodoc:
-
end
-
-
1
attr_reader :model_class, :fixture
-
-
1
def initialize(fixture, model_class)
-
@fixture = fixture
-
@model_class = model_class
-
end
-
-
1
def class_name
-
model_class.name if model_class
-
end
-
-
1
def each
-
fixture.each { |item| yield item }
-
end
-
-
1
def [](key)
-
fixture[key]
-
end
-
-
1
alias :to_hash :fixture
-
-
1
def find
-
if model_class
-
model_class.unscoped do
-
model_class.find(fixture[model_class.primary_key])
-
end
-
else
-
raise FixtureClassNotFound, "No class attached to find."
-
end
-
end
-
end
-
end
-
-
1
module ActiveRecord
-
1
module TestFixtures
-
1
extend ActiveSupport::Concern
-
-
1
def before_setup # :nodoc:
-
6
setup_fixtures
-
6
super
-
end
-
-
1
def after_teardown # :nodoc:
-
6
super
-
6
teardown_fixtures
-
end
-
-
1
included do
-
1
class_attribute :fixture_path, instance_writer: false
-
1
class_attribute :fixture_table_names, default: []
-
1
class_attribute :fixture_class_names, default: {}
-
1
class_attribute :use_transactional_tests, default: true
-
1
class_attribute :use_instantiated_fixtures, default: false # true, false, or :no_instances
-
1
class_attribute :pre_loaded_fixtures, default: false
-
1
class_attribute :config, default: ActiveRecord::Base
-
end
-
-
1
module ClassMethods
-
# Sets the model class for a fixture when the class name cannot be inferred from the fixture name.
-
#
-
# Examples:
-
#
-
# set_fixture_class some_fixture: SomeModel,
-
# 'namespaced/fixture' => Another::Model
-
#
-
# The keys must be the fixture names, that coincide with the short paths to the fixture files.
-
1
def set_fixture_class(class_names = {})
-
self.fixture_class_names = fixture_class_names.merge(class_names.stringify_keys)
-
end
-
-
1
def fixtures(*fixture_set_names)
-
1
if fixture_set_names.first == :all
-
1
fixture_set_names = Dir["#{fixture_path}/{**,*}/*.{yml}"].uniq
-
1
fixture_set_names.map! { |f| f[(fixture_path.to_s.size + 1)..-5] }
-
else
-
fixture_set_names = fixture_set_names.flatten.map(&:to_s)
-
end
-
-
1
self.fixture_table_names |= fixture_set_names
-
1
setup_fixture_accessors(fixture_set_names)
-
end
-
-
1
def setup_fixture_accessors(fixture_set_names = nil)
-
1
fixture_set_names = Array(fixture_set_names || fixture_table_names)
-
1
methods = Module.new do
-
1
fixture_set_names.each do |fs_name|
-
fs_name = fs_name.to_s
-
accessor_name = fs_name.tr("/", "_").to_sym
-
-
define_method(accessor_name) do |*fixture_names|
-
force_reload = fixture_names.pop if fixture_names.last == true || fixture_names.last == :reload
-
return_single_record = fixture_names.size == 1
-
fixture_names = @loaded_fixtures[fs_name].fixtures.keys if fixture_names.empty?
-
-
@fixture_cache[fs_name] ||= {}
-
-
instances = fixture_names.map do |f_name|
-
f_name = f_name.to_s if f_name.is_a?(Symbol)
-
@fixture_cache[fs_name].delete(f_name) if force_reload
-
-
if @loaded_fixtures[fs_name][f_name]
-
@fixture_cache[fs_name][f_name] ||= @loaded_fixtures[fs_name][f_name].find
-
else
-
raise StandardError, "No fixture named '#{f_name}' found for fixture set '#{fs_name}'"
-
end
-
end
-
-
return_single_record ? instances.first : instances
-
end
-
private accessor_name
-
end
-
end
-
1
include methods
-
end
-
-
1
def uses_transaction(*methods)
-
@uses_transaction = [] unless defined?(@uses_transaction)
-
@uses_transaction.concat methods.map(&:to_s)
-
end
-
-
1
def uses_transaction?(method)
-
12
@uses_transaction = [] unless defined?(@uses_transaction)
-
12
@uses_transaction.include?(method.to_s)
-
end
-
end
-
-
1
def run_in_transaction?
-
12
use_transactional_tests &&
-
11
!self.class.uses_transaction?(method_name)
-
end
-
-
1
def setup_fixtures(config = ActiveRecord::Base)
-
6
if pre_loaded_fixtures && !use_transactional_tests
-
raise RuntimeError, "pre_loaded_fixtures requires use_transactional_tests"
-
end
-
-
6
@fixture_cache = {}
-
6
@fixture_connections = []
-
6
@@already_loaded_fixtures ||= {}
-
6
@connection_subscriber = nil
-
-
# Load fixtures once and begin transaction.
-
6
if run_in_transaction?
-
6
if @@already_loaded_fixtures[self.class]
-
2
@loaded_fixtures = @@already_loaded_fixtures[self.class]
-
else
-
4
@loaded_fixtures = load_fixtures(config)
-
4
@@already_loaded_fixtures[self.class] = @loaded_fixtures
-
end
-
-
# Begin transactions for connections already established
-
6
@fixture_connections = enlist_fixture_connections
-
6
@fixture_connections.each do |connection|
-
6
connection.begin_transaction joinable: false
-
6
connection.pool.lock_thread = true
-
end
-
-
# When connections are established in the future, begin a transaction too
-
6
@connection_subscriber = ActiveSupport::Notifications.subscribe("!connection.active_record") do |_, _, _, _, payload|
-
spec_name = payload[:spec_name] if payload.key?(:spec_name)
-
-
if spec_name
-
begin
-
connection = ActiveRecord::Base.connection_handler.retrieve_connection(spec_name)
-
rescue ConnectionNotEstablished
-
connection = nil
-
end
-
-
if connection && !@fixture_connections.include?(connection)
-
connection.begin_transaction joinable: false
-
connection.pool.lock_thread = true
-
@fixture_connections << connection
-
end
-
end
-
end
-
-
# Load fixtures for every test.
-
else
-
ActiveRecord::FixtureSet.reset_cache
-
@@already_loaded_fixtures[self.class] = nil
-
@loaded_fixtures = load_fixtures(config)
-
end
-
-
# Instantiate fixtures for every test if requested.
-
6
instantiate_fixtures if use_instantiated_fixtures
-
end
-
-
1
def teardown_fixtures
-
# Rollback changes if a transaction is active.
-
6
if run_in_transaction?
-
6
ActiveSupport::Notifications.unsubscribe(@connection_subscriber) if @connection_subscriber
-
6
@fixture_connections.each do |connection|
-
6
connection.rollback_transaction if connection.transaction_open?
-
6
connection.pool.lock_thread = false
-
end
-
6
@fixture_connections.clear
-
else
-
ActiveRecord::FixtureSet.reset_cache
-
end
-
-
6
ActiveRecord::Base.clear_active_connections!
-
end
-
-
1
def enlist_fixture_connections
-
6
ActiveRecord::Base.connection_handler.connection_pool_list.map(&:connection)
-
end
-
-
1
private
-
1
def load_fixtures(config)
-
4
fixtures = ActiveRecord::FixtureSet.create_fixtures(fixture_path, fixture_table_names, fixture_class_names, config)
-
4
Hash[fixtures.map { |f| [f.name, f] }]
-
end
-
-
1
def instantiate_fixtures
-
if pre_loaded_fixtures
-
raise RuntimeError, "Load fixtures before instantiating them." if ActiveRecord::FixtureSet.all_loaded_fixtures.empty?
-
ActiveRecord::FixtureSet.instantiate_all_loaded_fixtures(self, load_instances?)
-
else
-
raise RuntimeError, "Load fixtures before instantiating them." if @loaded_fixtures.nil?
-
@loaded_fixtures.each_value do |fixture_set|
-
ActiveRecord::FixtureSet.instantiate_fixtures(self, fixture_set, load_instances?)
-
end
-
end
-
end
-
-
1
def load_instances?
-
use_instantiated_fixtures != :no_instances
-
end
-
end
-
end
-
-
1
class ActiveRecord::FixtureSet::RenderContext # :nodoc:
-
1
def self.create_subclass
-
Class.new ActiveRecord::FixtureSet.context_class do
-
def get_binding
-
binding()
-
end
-
-
def binary(path)
-
%(!!binary "#{Base64.strict_encode64(File.read(path))}")
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_support/core_ext/module/attr_internal"
-
1
require "active_record/log_subscriber"
-
-
1
module ActiveRecord
-
1
module Railties # :nodoc:
-
1
module ControllerRuntime #:nodoc:
-
1
extend ActiveSupport::Concern
-
-
# TODO Change this to private once we've dropped Ruby 2.2 support.
-
# Workaround for Ruby 2.2 "private attribute?" warning.
-
1
protected
-
-
1
attr_internal :db_runtime
-
-
1
private
-
-
1
def process_action(action, *args)
-
# We also need to reset the runtime before each action
-
# because of queries in middleware or in cases we are streaming
-
# and it won't be cleaned up by the method below.
-
2
ActiveRecord::LogSubscriber.reset_runtime
-
2
super
-
end
-
-
1
def cleanup_view_runtime
-
2
if logger && logger.info? && ActiveRecord::Base.connected?
-
2
db_rt_before_render = ActiveRecord::LogSubscriber.reset_runtime
-
2
self.db_runtime = (db_runtime || 0) + db_rt_before_render
-
2
runtime = super
-
2
db_rt_after_render = ActiveRecord::LogSubscriber.reset_runtime
-
2
self.db_runtime += db_rt_after_render
-
2
runtime - db_rt_after_render
-
else
-
super
-
end
-
end
-
-
1
def append_info_to_payload(payload)
-
2
super
-
2
if ActiveRecord::Base.connected?
-
2
payload[:db_runtime] = (db_runtime || 0) + ActiveRecord::LogSubscriber.reset_runtime
-
end
-
end
-
-
1
module ClassMethods # :nodoc:
-
1
def log_process_action(payload)
-
2
messages, db_runtime = super, payload[:db_runtime]
-
2
messages << ("ActiveRecord: %.1fms" % db_runtime.to_f) if db_runtime
-
2
messages
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActiveRecord
-
# = Active Record \Relation
-
1
class Relation
-
1
MULTI_VALUE_METHODS = [:includes, :eager_load, :preload, :select, :group,
-
:order, :joins, :left_outer_joins, :references,
-
:extending, :unscope]
-
-
1
SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :reordering,
-
:reverse_order, :distinct, :create_with, :skip_query_cache]
-
1
CLAUSE_METHODS = [:where, :having, :from]
-
1
INVALID_METHODS_FOR_DELETE_ALL = [:distinct, :group, :having]
-
-
1
VALUE_METHODS = MULTI_VALUE_METHODS + SINGLE_VALUE_METHODS + CLAUSE_METHODS
-
-
1
include Enumerable
-
1
include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches, Explain, Delegation
-
-
1
attr_reader :table, :klass, :loaded, :predicate_builder
-
1
alias :model :klass
-
1
alias :loaded? :loaded
-
1
alias :locked? :lock_value
-
-
3
def initialize(klass, table: klass.arel_table, predicate_builder: klass.predicate_builder, values: {})
-
2
@klass = klass
-
2
@table = table
-
2
@values = values
-
2
@offsets = {}
-
2
@loaded = false
-
2
@predicate_builder = predicate_builder
-
2
@delegate_to_klass = false
-
end
-
-
1
def initialize_copy(other)
-
4
@values = @values.dup
-
4
reset
-
end
-
-
1
def arel_attribute(name) # :nodoc:
-
4
klass.arel_attribute(name, table)
-
end
-
-
# Initializes new record from relation while maintaining the current
-
# scope.
-
#
-
# Expects arguments in the same format as {ActiveRecord::Base.new}[rdoc-ref:Core.new].
-
#
-
# users = User.where(name: 'DHH')
-
# user = users.new # => #<User id: nil, name: "DHH", created_at: nil, updated_at: nil>
-
#
-
# You can also pass a block to new with the new record as argument:
-
#
-
# user = users.new { |user| user.name = 'Oscar' }
-
# user.name # => Oscar
-
1
def new(attributes = nil, &block)
-
scoping { klass.new(values_for_create(attributes), &block) }
-
end
-
-
1
alias build new
-
-
# Tries to create a new record with the same scoped attributes
-
# defined in the relation. Returns the initialized object if validation fails.
-
#
-
# Expects arguments in the same format as
-
# {ActiveRecord::Base.create}[rdoc-ref:Persistence::ClassMethods#create].
-
#
-
# ==== Examples
-
#
-
# users = User.where(name: 'Oscar')
-
# users.create # => #<User id: 3, name: "Oscar", ...>
-
#
-
# users.create(name: 'fxn')
-
# users.create # => #<User id: 4, name: "fxn", ...>
-
#
-
# users.create { |user| user.name = 'tenderlove' }
-
# # => #<User id: 5, name: "tenderlove", ...>
-
#
-
# users.create(name: nil) # validation on name
-
# # => #<User id: nil, name: nil, ...>
-
1
def create(attributes = nil, &block)
-
if attributes.is_a?(Array)
-
attributes.collect { |attr| create(attr, &block) }
-
else
-
scoping { klass.create(values_for_create(attributes), &block) }
-
end
-
end
-
-
# Similar to #create, but calls
-
# {create!}[rdoc-ref:Persistence::ClassMethods#create!]
-
# on the base class. Raises an exception if a validation error occurs.
-
#
-
# Expects arguments in the same format as
-
# {ActiveRecord::Base.create!}[rdoc-ref:Persistence::ClassMethods#create!].
-
1
def create!(attributes = nil, &block)
-
if attributes.is_a?(Array)
-
attributes.collect { |attr| create!(attr, &block) }
-
else
-
scoping { klass.create!(values_for_create(attributes), &block) }
-
end
-
end
-
-
1
def first_or_create(attributes = nil, &block) # :nodoc:
-
first || create(attributes, &block)
-
end
-
-
1
def first_or_create!(attributes = nil, &block) # :nodoc:
-
first || create!(attributes, &block)
-
end
-
-
1
def first_or_initialize(attributes = nil, &block) # :nodoc:
-
first || new(attributes, &block)
-
end
-
-
# Finds the first record with the given attributes, or creates a record
-
# with the attributes if one is not found:
-
#
-
# # Find the first user named "Pen��lope" or create a new one.
-
# User.find_or_create_by(first_name: 'Pen��lope')
-
# # => #<User id: 1, first_name: "Pen��lope", last_name: nil>
-
#
-
# # Find the first user named "Pen��lope" or create a new one.
-
# # We already have one so the existing record will be returned.
-
# User.find_or_create_by(first_name: 'Pen��lope')
-
# # => #<User id: 1, first_name: "Pen��lope", last_name: nil>
-
#
-
# # Find the first user named "Scarlett" or create a new one with
-
# # a particular last name.
-
# User.create_with(last_name: 'Johansson').find_or_create_by(first_name: 'Scarlett')
-
# # => #<User id: 2, first_name: "Scarlett", last_name: "Johansson">
-
#
-
# This method accepts a block, which is passed down to #create. The last example
-
# above can be alternatively written this way:
-
#
-
# # Find the first user named "Scarlett" or create a new one with a
-
# # different last name.
-
# User.find_or_create_by(first_name: 'Scarlett') do |user|
-
# user.last_name = 'Johansson'
-
# end
-
# # => #<User id: 2, first_name: "Scarlett", last_name: "Johansson">
-
#
-
# This method always returns a record, but if creation was attempted and
-
# failed due to validation errors it won't be persisted, you get what
-
# #create returns in such situation.
-
#
-
# Please note *this method is not atomic*, it runs first a SELECT, and if
-
# there are no results an INSERT is attempted. If there are other threads
-
# or processes there is a race condition between both calls and it could
-
# be the case that you end up with two similar records.
-
#
-
# Whether that is a problem or not depends on the logic of the
-
# application, but in the particular case in which rows have a UNIQUE
-
# constraint an exception may be raised, just retry:
-
#
-
# begin
-
# CreditAccount.transaction(requires_new: true) do
-
# CreditAccount.find_or_create_by(user_id: user.id)
-
# end
-
# rescue ActiveRecord::RecordNotUnique
-
# retry
-
# end
-
#
-
1
def find_or_create_by(attributes, &block)
-
find_by(attributes) || create(attributes, &block)
-
end
-
-
# Like #find_or_create_by, but calls
-
# {create!}[rdoc-ref:Persistence::ClassMethods#create!] so an exception
-
# is raised if the created record is invalid.
-
1
def find_or_create_by!(attributes, &block)
-
find_by(attributes) || create!(attributes, &block)
-
end
-
-
# Like #find_or_create_by, but calls {new}[rdoc-ref:Core#new]
-
# instead of {create}[rdoc-ref:Persistence::ClassMethods#create].
-
1
def find_or_initialize_by(attributes, &block)
-
find_by(attributes) || new(attributes, &block)
-
end
-
-
# Runs EXPLAIN on the query or queries triggered by this relation and
-
# returns the result as a string. The string is formatted imitating the
-
# ones printed by the database shell.
-
#
-
# Note that this method actually runs the queries, since the results of some
-
# are needed by the next ones when eager loading is going on.
-
#
-
# Please see further details in the
-
# {Active Record Query Interface guide}[http://guides.rubyonrails.org/active_record_querying.html#running-explain].
-
1
def explain
-
exec_explain(collecting_queries_for_explain { exec_queries })
-
end
-
-
# Converts relation objects to Array.
-
1
def to_ary
-
records.dup
-
end
-
1
alias to_a to_ary
-
-
1
def records # :nodoc:
-
load
-
@records
-
end
-
-
# Serializes the relation objects Array.
-
1
def encode_with(coder)
-
coder.represent_seq(nil, records)
-
end
-
-
# Returns size of the records.
-
1
def size
-
loaded? ? @records.length : count(:all)
-
end
-
-
# Returns true if there are no records.
-
1
def empty?
-
return @records.empty? if loaded?
-
!exists?
-
end
-
-
# Returns true if there are no records.
-
1
def none?
-
return super if block_given?
-
empty?
-
end
-
-
# Returns true if there are any records.
-
1
def any?
-
return super if block_given?
-
!empty?
-
end
-
-
# Returns true if there is exactly one record.
-
1
def one?
-
return super if block_given?
-
limit_value ? records.one? : size == 1
-
end
-
-
# Returns true if there is more than one record.
-
1
def many?
-
return super if block_given?
-
limit_value ? records.many? : size > 1
-
end
-
-
# Returns a cache key that can be used to identify the records fetched by
-
# this query. The cache key is built with a fingerprint of the sql query,
-
# the number of records matched by the query and a timestamp of the last
-
# updated record. When a new record comes to match the query, or any of
-
# the existing records is updated or deleted, the cache key changes.
-
#
-
# Product.where("name like ?", "%Cosmic Encounter%").cache_key
-
# # => "products/query-1850ab3d302391b85b8693e941286659-1-20150714212553907087000"
-
#
-
# If the collection is loaded, the method will iterate through the records
-
# to generate the timestamp, otherwise it will trigger one SQL query like:
-
#
-
# SELECT COUNT(*), MAX("products"."updated_at") FROM "products" WHERE (name like '%Cosmic Encounter%')
-
#
-
# You can also pass a custom timestamp column to fetch the timestamp of the
-
# last updated record.
-
#
-
# Product.where("name like ?", "%Game%").cache_key(:last_reviewed_at)
-
#
-
# You can customize the strategy to generate the key on a per model basis
-
# overriding ActiveRecord::Base#collection_cache_key.
-
1
def cache_key(timestamp_column = :updated_at)
-
@cache_keys ||= {}
-
@cache_keys[timestamp_column] ||= @klass.collection_cache_key(self, timestamp_column)
-
end
-
-
# Scope all queries to the current scope.
-
#
-
# Comment.where(post_id: 1).scoping do
-
# Comment.first
-
# end
-
# # => SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = 1 ORDER BY "comments"."id" ASC LIMIT 1
-
#
-
# Please check unscoped if you want to remove all previous scopes (including
-
# the default_scope) during the execution of a block.
-
1
def scoping
-
previous, klass.current_scope = klass.current_scope(true), self unless @delegate_to_klass
-
yield
-
ensure
-
klass.current_scope = previous unless @delegate_to_klass
-
end
-
-
1
def _exec_scope(*args, &block) # :nodoc:
-
@delegate_to_klass = true
-
instance_exec(*args, &block) || self
-
ensure
-
@delegate_to_klass = false
-
end
-
-
# Updates all records in the current relation with details given. This method constructs a single SQL UPDATE
-
# statement and sends it straight to the database. It does not instantiate the involved models and it does not
-
# trigger Active Record callbacks or validations. However, values passed to #update_all will still go through
-
# Active Record's normal type casting and serialization.
-
#
-
# ==== Parameters
-
#
-
# * +updates+ - A string, array, or hash representing the SET part of an SQL statement.
-
#
-
# ==== Examples
-
#
-
# # Update all customers with the given attributes
-
# Customer.update_all wants_email: true
-
#
-
# # Update all books with 'Rails' in their title
-
# Book.where('title LIKE ?', '%Rails%').update_all(author: 'David')
-
#
-
# # Update all books that match conditions, but limit it to 5 ordered by date
-
# Book.where('title LIKE ?', '%Rails%').order(:created_at).limit(5).update_all(author: 'David')
-
#
-
# # Update all invoices and set the number column to its id value.
-
# Invoice.update_all('number = id')
-
1
def update_all(updates)
-
raise ArgumentError, "Empty list of attributes to change" if updates.blank?
-
-
if eager_loading?
-
relation = apply_join_dependency
-
return relation.update_all(updates)
-
end
-
-
stmt = Arel::UpdateManager.new
-
-
stmt.set Arel.sql(@klass.sanitize_sql_for_assignment(updates))
-
stmt.table(table)
-
-
if has_join_values? || offset_value
-
@klass.connection.join_to_update(stmt, arel, arel_attribute(primary_key))
-
else
-
stmt.key = arel_attribute(primary_key)
-
stmt.take(arel.limit)
-
stmt.order(*arel.orders)
-
stmt.wheres = arel.constraints
-
end
-
-
@klass.connection.update stmt, "#{@klass} Update All"
-
end
-
-
1
def update(id = :all, attributes) # :nodoc:
-
if id == :all
-
each { |record| record.update(attributes) }
-
else
-
klass.update(id, attributes)
-
end
-
end
-
-
# Destroys the records by instantiating each
-
# record and calling its {#destroy}[rdoc-ref:Persistence#destroy] method.
-
# Each object's callbacks are executed (including <tt>:dependent</tt> association options).
-
# Returns the collection of objects that were destroyed; each will be frozen, to
-
# reflect that no changes should be made (since they can't be persisted).
-
#
-
# Note: Instantiation, callback execution, and deletion of each
-
# record can be time consuming when you're removing many records at
-
# once. It generates at least one SQL +DELETE+ query per record (or
-
# possibly more, to enforce your callbacks). If you want to delete many
-
# rows quickly, without concern for their associations or callbacks, use
-
# #delete_all instead.
-
#
-
# ==== Examples
-
#
-
# Person.where(age: 0..18).destroy_all
-
1
def destroy_all
-
records.each(&:destroy).tap { reset }
-
end
-
-
# Deletes the records without instantiating the records
-
# first, and hence not calling the {#destroy}[rdoc-ref:Persistence#destroy]
-
# method nor invoking callbacks.
-
# This is a single SQL DELETE statement that goes straight to the database, much more
-
# efficient than #destroy_all. Be careful with relations though, in particular
-
# <tt>:dependent</tt> rules defined on associations are not honored. Returns the
-
# number of rows affected.
-
#
-
# Post.where(person_id: 5).where(category: ['Something', 'Else']).delete_all
-
#
-
# Both calls delete the affected posts all at once with a single DELETE statement.
-
# If you need to destroy dependent associations or call your <tt>before_*</tt> or
-
# +after_destroy+ callbacks, use the #destroy_all method instead.
-
#
-
# If an invalid method is supplied, #delete_all raises an ActiveRecordError:
-
#
-
# Post.distinct.delete_all
-
# # => ActiveRecord::ActiveRecordError: delete_all doesn't support distinct
-
1
def delete_all
-
invalid_methods = INVALID_METHODS_FOR_DELETE_ALL.select do |method|
-
value = get_value(method)
-
SINGLE_VALUE_METHODS.include?(method) ? value : value.any?
-
end
-
if invalid_methods.any?
-
raise ActiveRecordError.new("delete_all doesn't support #{invalid_methods.join(', ')}")
-
end
-
-
if eager_loading?
-
relation = apply_join_dependency
-
return relation.delete_all
-
end
-
-
stmt = Arel::DeleteManager.new
-
stmt.from(table)
-
-
if has_join_values? || has_limit_or_offset?
-
@klass.connection.join_to_delete(stmt, arel, arel_attribute(primary_key))
-
else
-
stmt.wheres = arel.constraints
-
end
-
-
affected = @klass.connection.delete(stmt, "#{@klass} Destroy")
-
-
reset
-
affected
-
end
-
-
# Causes the records to be loaded from the database if they have not
-
# been loaded already. You can use this if for some reason you need
-
# to explicitly load some records before actually using them. The
-
# return value is the relation itself, not the records.
-
#
-
# Post.where(published: true).load # => #<ActiveRecord::Relation>
-
1
def load(&block)
-
exec_queries(&block) unless loaded?
-
-
self
-
end
-
-
# Forces reloading of relation.
-
1
def reload
-
reset
-
load
-
end
-
-
1
def reset
-
4
@delegate_to_klass = false
-
4
@to_sql = @arel = @loaded = @should_eager_load = nil
-
4
@records = [].freeze
-
4
@offsets = {}
-
4
self
-
end
-
-
# Returns sql statement for the relation.
-
#
-
# User.where(name: 'Oscar').to_sql
-
# # => SELECT "users".* FROM "users" WHERE "users"."name" = 'Oscar'
-
1
def to_sql
-
@to_sql ||= begin
-
if eager_loading?
-
apply_join_dependency do |relation, join_dependency|
-
relation = join_dependency.apply_column_aliases(relation)
-
relation.to_sql
-
end
-
else
-
conn = klass.connection
-
conn.unprepared_statement { conn.to_sql(arel) }
-
end
-
end
-
end
-
-
# Returns a hash of where conditions.
-
#
-
# User.where(name: 'Oscar').where_values_hash
-
# # => {name: "Oscar"}
-
1
def where_values_hash(relation_table_name = klass.table_name)
-
where_clause.to_h(relation_table_name)
-
end
-
-
1
def scope_for_create
-
where_values_hash.merge!(create_with_value.stringify_keys)
-
end
-
-
# Returns true if relation needs eager loading.
-
1
def eager_loading?
-
2
@should_eager_load ||=
-
1
eager_load_values.any? ||
-
2
includes_values.any? && (joined_includes_values.any? || references_eager_loaded_tables?)
-
end
-
-
# Joins that are also marked for preloading. In which case we should just eager load them.
-
# Note that this is a naive implementation because we could have strings and symbols which
-
# represent the same association, but that aren't matched by this. Also, we could have
-
# nested hashes which partially match, e.g. { a: :b } & { a: [:b, :c] }
-
1
def joined_includes_values
-
includes_values & joins_values
-
end
-
-
# Compares two relations for equality.
-
1
def ==(other)
-
case other
-
when Associations::CollectionProxy, AssociationRelation
-
self == other.records
-
when Relation
-
other.to_sql == to_sql
-
when Array
-
records == other
-
end
-
end
-
-
1
def pretty_print(q)
-
q.pp(records)
-
end
-
-
# Returns true if relation is blank.
-
1
def blank?
-
records.blank?
-
end
-
-
1
def values
-
@values.dup
-
end
-
-
1
def inspect
-
subject = loaded? ? records : self
-
entries = subject.take([limit_value, 11].compact.min).map!(&:inspect)
-
-
entries[10] = "..." if entries.size == 11
-
-
"#<#{self.class.name} [#{entries.join(', ')}]>"
-
end
-
-
1
def empty_scope? # :nodoc:
-
@values == klass.unscoped.values
-
end
-
-
1
def has_limit_or_offset? # :nodoc:
-
limit_value || offset_value
-
end
-
-
1
def alias_tracker(joins = [], aliases = nil) # :nodoc:
-
joins += [aliases] if aliases
-
ActiveRecord::Associations::AliasTracker.create(connection, table.name, joins)
-
end
-
-
1
protected
-
-
1
def load_records(records)
-
@records = records.freeze
-
@loaded = true
-
end
-
-
1
private
-
-
1
def has_join_values?
-
joins_values.any? || left_outer_joins_values.any?
-
end
-
-
1
def exec_queries(&block)
-
skip_query_cache_if_necessary do
-
@records =
-
if eager_loading?
-
apply_join_dependency do |relation, join_dependency|
-
if ActiveRecord::NullRelation === relation
-
[]
-
else
-
relation = join_dependency.apply_column_aliases(relation)
-
rows = connection.select_all(relation.arel, "SQL")
-
join_dependency.instantiate(rows, &block)
-
end.freeze
-
end
-
else
-
klass.find_by_sql(arel, &block).freeze
-
end
-
-
preload = preload_values
-
preload += includes_values unless eager_loading?
-
preloader = nil
-
preload.each do |associations|
-
preloader ||= build_preloader
-
preloader.preload @records, associations
-
end
-
-
@records.each(&:readonly!) if readonly_value
-
-
@loaded = true
-
@records
-
end
-
end
-
-
1
def skip_query_cache_if_necessary
-
2
if skip_query_cache_value
-
uncached do
-
yield
-
end
-
else
-
2
yield
-
end
-
end
-
-
1
def build_preloader
-
ActiveRecord::Associations::Preloader.new
-
end
-
-
1
def references_eager_loaded_tables?
-
joined_tables = arel.join_sources.map do |join|
-
if join.is_a?(Arel::Nodes::StringJoin)
-
tables_in_string(join.left)
-
else
-
[join.left.table_name, join.left.table_alias]
-
end
-
end
-
-
joined_tables += [table.name, table.table_alias]
-
-
# always convert table names to downcase as in Oracle quoted table names are in uppercase
-
joined_tables = joined_tables.flatten.compact.map(&:downcase).uniq
-
-
(references_values - joined_tables).any?
-
end
-
-
1
def tables_in_string(string)
-
return [] if string.blank?
-
# always convert table names to downcase as in Oracle quoted table names are in uppercase
-
# ignore raw_sql_ that is used by Oracle adapter as alias for limit/offset subqueries
-
string.scan(/([a-zA-Z_][.\w]+).?\./).flatten.map(&:downcase).uniq - ["raw_sql_"]
-
end
-
-
1
def values_for_create(attributes = nil)
-
result = attributes ? where_values_hash.merge!(attributes) : where_values_hash
-
-
# NOTE: if there are same keys in both create_with and result, create_with should be used.
-
# This is to make sure nested attributes don't get passed to the klass.new,
-
# while keeping the precedence of the duplicate keys in create_with.
-
create_with_value.stringify_keys.each do |k, v|
-
result[k] = v if result.key?(k)
-
end
-
-
result
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_record/relation/batches/batch_enumerator"
-
-
1
module ActiveRecord
-
1
module Batches
-
1
ORDER_IGNORE_MESSAGE = "Scoped order is ignored, it's forced to be batch order."
-
-
# Looping through a collection of records from the database
-
# (using the Scoping::Named::ClassMethods.all method, for example)
-
# is very inefficient since it will try to instantiate all the objects at once.
-
#
-
# In that case, batch processing methods allow you to work
-
# with the records in batches, thereby greatly reducing memory consumption.
-
#
-
# The #find_each method uses #find_in_batches with a batch size of 1000 (or as
-
# specified by the +:batch_size+ option).
-
#
-
# Person.find_each do |person|
-
# person.do_awesome_stuff
-
# end
-
#
-
# Person.where("age > 21").find_each do |person|
-
# person.party_all_night!
-
# end
-
#
-
# If you do not provide a block to #find_each, it will return an Enumerator
-
# for chaining with other methods:
-
#
-
# Person.find_each.with_index do |person, index|
-
# person.award_trophy(index + 1)
-
# end
-
#
-
# ==== Options
-
# * <tt>:batch_size</tt> - Specifies the size of the batch. Defaults to 1000.
-
# * <tt>:start</tt> - Specifies the primary key value to start from, inclusive of the value.
-
# * <tt>:finish</tt> - Specifies the primary key value to end at, inclusive of the value.
-
# * <tt>:error_on_ignore</tt> - Overrides the application config to specify if an error should be raised when
-
# an order is present in the relation.
-
#
-
# Limits are honored, and if present there is no requirement for the batch
-
# size: it can be less than, equal to, or greater than the limit.
-
#
-
# The options +start+ and +finish+ are especially useful if you want
-
# multiple workers dealing with the same processing queue. You can make
-
# worker 1 handle all the records between id 1 and 9999 and worker 2
-
# handle from 10000 and beyond by setting the +:start+ and +:finish+
-
# option on each worker.
-
#
-
# # In worker 1, let's process until 9999 records.
-
# Person.find_each(finish: 9_999) do |person|
-
# person.party_all_night!
-
# end
-
#
-
# # In worker 2, let's process from record 10_000 and onwards.
-
# Person.find_each(start: 10_000) do |person|
-
# person.party_all_night!
-
# end
-
#
-
# NOTE: It's not possible to set the order. That is automatically set to
-
# ascending on the primary key ("id ASC") to make the batch ordering
-
# work. This also means that this method only works when the primary key is
-
# orderable (e.g. an integer or string).
-
#
-
# NOTE: By its nature, batch processing is subject to race conditions if
-
# other processes are modifying the database.
-
1
def find_each(start: nil, finish: nil, batch_size: 1000, error_on_ignore: nil)
-
if block_given?
-
find_in_batches(start: start, finish: finish, batch_size: batch_size, error_on_ignore: error_on_ignore) do |records|
-
records.each { |record| yield record }
-
end
-
else
-
enum_for(:find_each, start: start, finish: finish, batch_size: batch_size, error_on_ignore: error_on_ignore) do
-
relation = self
-
apply_limits(relation, start, finish).size
-
end
-
end
-
end
-
-
# Yields each batch of records that was found by the find options as
-
# an array.
-
#
-
# Person.where("age > 21").find_in_batches do |group|
-
# sleep(50) # Make sure it doesn't get too crowded in there!
-
# group.each { |person| person.party_all_night! }
-
# end
-
#
-
# If you do not provide a block to #find_in_batches, it will return an Enumerator
-
# for chaining with other methods:
-
#
-
# Person.find_in_batches.with_index do |group, batch|
-
# puts "Processing group ##{batch}"
-
# group.each(&:recover_from_last_night!)
-
# end
-
#
-
# To be yielded each record one by one, use #find_each instead.
-
#
-
# ==== Options
-
# * <tt>:batch_size</tt> - Specifies the size of the batch. Defaults to 1000.
-
# * <tt>:start</tt> - Specifies the primary key value to start from, inclusive of the value.
-
# * <tt>:finish</tt> - Specifies the primary key value to end at, inclusive of the value.
-
# * <tt>:error_on_ignore</tt> - Overrides the application config to specify if an error should be raised when
-
# an order is present in the relation.
-
#
-
# Limits are honored, and if present there is no requirement for the batch
-
# size: it can be less than, equal to, or greater than the limit.
-
#
-
# The options +start+ and +finish+ are especially useful if you want
-
# multiple workers dealing with the same processing queue. You can make
-
# worker 1 handle all the records between id 1 and 9999 and worker 2
-
# handle from 10000 and beyond by setting the +:start+ and +:finish+
-
# option on each worker.
-
#
-
# # Let's process from record 10_000 on.
-
# Person.find_in_batches(start: 10_000) do |group|
-
# group.each { |person| person.party_all_night! }
-
# end
-
#
-
# NOTE: It's not possible to set the order. That is automatically set to
-
# ascending on the primary key ("id ASC") to make the batch ordering
-
# work. This also means that this method only works when the primary key is
-
# orderable (e.g. an integer or string).
-
#
-
# NOTE: By its nature, batch processing is subject to race conditions if
-
# other processes are modifying the database.
-
1
def find_in_batches(start: nil, finish: nil, batch_size: 1000, error_on_ignore: nil)
-
relation = self
-
unless block_given?
-
return to_enum(:find_in_batches, start: start, finish: finish, batch_size: batch_size, error_on_ignore: error_on_ignore) do
-
total = apply_limits(relation, start, finish).size
-
(total - 1).div(batch_size) + 1
-
end
-
end
-
-
in_batches(of: batch_size, start: start, finish: finish, load: true, error_on_ignore: error_on_ignore) do |batch|
-
yield batch.to_a
-
end
-
end
-
-
# Yields ActiveRecord::Relation objects to work with a batch of records.
-
#
-
# Person.where("age > 21").in_batches do |relation|
-
# relation.delete_all
-
# sleep(10) # Throttle the delete queries
-
# end
-
#
-
# If you do not provide a block to #in_batches, it will return a
-
# BatchEnumerator which is enumerable.
-
#
-
# Person.in_batches.each_with_index do |relation, batch_index|
-
# puts "Processing relation ##{batch_index}"
-
# relation.delete_all
-
# end
-
#
-
# Examples of calling methods on the returned BatchEnumerator object:
-
#
-
# Person.in_batches.delete_all
-
# Person.in_batches.update_all(awesome: true)
-
# Person.in_batches.each_record(&:party_all_night!)
-
#
-
# ==== Options
-
# * <tt>:of</tt> - Specifies the size of the batch. Defaults to 1000.
-
# * <tt>:load</tt> - Specifies if the relation should be loaded. Defaults to false.
-
# * <tt>:start</tt> - Specifies the primary key value to start from, inclusive of the value.
-
# * <tt>:finish</tt> - Specifies the primary key value to end at, inclusive of the value.
-
# * <tt>:error_on_ignore</tt> - Overrides the application config to specify if an error should be raised when
-
# an order is present in the relation.
-
#
-
# Limits are honored, and if present there is no requirement for the batch
-
# size, it can be less than, equal, or greater than the limit.
-
#
-
# The options +start+ and +finish+ are especially useful if you want
-
# multiple workers dealing with the same processing queue. You can make
-
# worker 1 handle all the records between id 1 and 9999 and worker 2
-
# handle from 10000 and beyond by setting the +:start+ and +:finish+
-
# option on each worker.
-
#
-
# # Let's process from record 10_000 on.
-
# Person.in_batches(start: 10_000).update_all(awesome: true)
-
#
-
# An example of calling where query method on the relation:
-
#
-
# Person.in_batches.each do |relation|
-
# relation.update_all('age = age + 1')
-
# relation.where('age > 21').update_all(should_party: true)
-
# relation.where('age <= 21').delete_all
-
# end
-
#
-
# NOTE: If you are going to iterate through each record, you should call
-
# #each_record on the yielded BatchEnumerator:
-
#
-
# Person.in_batches.each_record(&:party_all_night!)
-
#
-
# NOTE: It's not possible to set the order. That is automatically set to
-
# ascending on the primary key ("id ASC") to make the batch ordering
-
# consistent. Therefore the primary key must be orderable, e.g. an integer
-
# or a string.
-
#
-
# NOTE: By its nature, batch processing is subject to race conditions if
-
# other processes are modifying the database.
-
1
def in_batches(of: 1000, start: nil, finish: nil, load: false, error_on_ignore: nil)
-
relation = self
-
unless block_given?
-
return BatchEnumerator.new(of: of, start: start, finish: finish, relation: self)
-
end
-
-
if arel.orders.present?
-
act_on_ignored_order(error_on_ignore)
-
end
-
-
batch_limit = of
-
if limit_value
-
remaining = limit_value
-
batch_limit = remaining if remaining < batch_limit
-
end
-
-
relation = relation.reorder(batch_order).limit(batch_limit)
-
relation = apply_limits(relation, start, finish)
-
relation.skip_query_cache! # Retaining the results in the query cache would undermine the point of batching
-
batch_relation = relation
-
-
loop do
-
if load
-
records = batch_relation.records
-
ids = records.map(&:id)
-
yielded_relation = where(primary_key => ids)
-
yielded_relation.load_records(records)
-
else
-
ids = batch_relation.pluck(primary_key)
-
yielded_relation = where(primary_key => ids)
-
end
-
-
break if ids.empty?
-
-
primary_key_offset = ids.last
-
raise ArgumentError.new("Primary key not included in the custom select clause") unless primary_key_offset
-
-
yield yielded_relation
-
-
break if ids.length < batch_limit
-
-
if limit_value
-
remaining -= ids.length
-
-
if remaining == 0
-
# Saves a useless iteration when the limit is a multiple of the
-
# batch size.
-
break
-
elsif remaining < batch_limit
-
relation = relation.limit(remaining)
-
end
-
end
-
-
attr = Relation::QueryAttribute.new(primary_key, primary_key_offset, klass.type_for_attribute(primary_key))
-
batch_relation = relation.where(arel_attribute(primary_key).gt(Arel::Nodes::BindParam.new(attr)))
-
end
-
end
-
-
1
private
-
-
1
def apply_limits(relation, start, finish)
-
if start
-
attr = Relation::QueryAttribute.new(primary_key, start, klass.type_for_attribute(primary_key))
-
relation = relation.where(arel_attribute(primary_key).gteq(Arel::Nodes::BindParam.new(attr)))
-
end
-
if finish
-
attr = Relation::QueryAttribute.new(primary_key, finish, klass.type_for_attribute(primary_key))
-
relation = relation.where(arel_attribute(primary_key).lteq(Arel::Nodes::BindParam.new(attr)))
-
end
-
relation
-
end
-
-
1
def batch_order
-
arel_attribute(primary_key).asc
-
end
-
-
1
def act_on_ignored_order(error_on_ignore)
-
raise_error = (error_on_ignore.nil? ? klass.error_on_ignored_order : error_on_ignore)
-
-
if raise_error
-
raise ArgumentError.new(ORDER_IGNORE_MESSAGE)
-
elsif logger
-
logger.warn(ORDER_IGNORE_MESSAGE)
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActiveRecord
-
1
module Batches
-
1
class BatchEnumerator
-
1
include Enumerable
-
-
1
def initialize(of: 1000, start: nil, finish: nil, relation:) #:nodoc:
-
@of = of
-
@relation = relation
-
@start = start
-
@finish = finish
-
end
-
-
# Looping through a collection of records from the database (using the
-
# +all+ method, for example) is very inefficient since it will try to
-
# instantiate all the objects at once.
-
#
-
# In that case, batch processing methods allow you to work with the
-
# records in batches, thereby greatly reducing memory consumption.
-
#
-
# Person.in_batches.each_record do |person|
-
# person.do_awesome_stuff
-
# end
-
#
-
# Person.where("age > 21").in_batches(of: 10).each_record do |person|
-
# person.party_all_night!
-
# end
-
#
-
# If you do not provide a block to #each_record, it will return an Enumerator
-
# for chaining with other methods:
-
#
-
# Person.in_batches.each_record.with_index do |person, index|
-
# person.award_trophy(index + 1)
-
# end
-
1
def each_record
-
return to_enum(:each_record) unless block_given?
-
-
@relation.to_enum(:in_batches, of: @of, start: @start, finish: @finish, load: true).each do |relation|
-
relation.records.each { |record| yield record }
-
end
-
end
-
-
# Delegates #delete_all, #update_all, #destroy_all methods to each batch.
-
#
-
# People.in_batches.delete_all
-
# People.where('age < 10').in_batches.destroy_all
-
# People.in_batches.update_all('age = age + 1')
-
1
[:delete_all, :update_all, :destroy_all].each do |method|
-
3
define_method(method) do |*args, &block|
-
@relation.to_enum(:in_batches, of: @of, start: @start, finish: @finish, load: false).each do |relation|
-
relation.send(method, *args, &block)
-
end
-
end
-
end
-
-
# Yields an ActiveRecord::Relation object for each batch of records.
-
#
-
# Person.in_batches.each do |relation|
-
# relation.update_all(awesome: true)
-
# end
-
1
def each
-
enum = @relation.to_enum(:in_batches, of: @of, start: @start, finish: @finish, load: false)
-
return enum.each { |relation| yield relation } if block_given?
-
enum
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActiveRecord
-
1
module Calculations
-
# Count the records.
-
#
-
# Person.count
-
# # => the total count of all people
-
#
-
# Person.count(:age)
-
# # => returns the total count of all people whose age is present in database
-
#
-
# Person.count(:all)
-
# # => performs a COUNT(*) (:all is an alias for '*')
-
#
-
# Person.distinct.count(:age)
-
# # => counts the number of different age values
-
#
-
# If #count is used with {Relation#group}[rdoc-ref:QueryMethods#group],
-
# it returns a Hash whose keys represent the aggregated column,
-
# and the values are the respective amounts:
-
#
-
# Person.group(:city).count
-
# # => { 'Rome' => 5, 'Paris' => 3 }
-
#
-
# If #count is used with {Relation#group}[rdoc-ref:QueryMethods#group] for multiple columns, it returns a Hash whose
-
# keys are an array containing the individual values of each column and the value
-
# of each key would be the #count.
-
#
-
# Article.group(:status, :category).count
-
# # => {["draft", "business"]=>10, ["draft", "technology"]=>4,
-
# ["published", "business"]=>0, ["published", "technology"]=>2}
-
#
-
# If #count is used with {Relation#select}[rdoc-ref:QueryMethods#select], it will count the selected columns:
-
#
-
# Person.select(:age).count
-
# # => counts the number of different age values
-
#
-
# Note: not all valid {Relation#select}[rdoc-ref:QueryMethods#select] expressions are valid #count expressions. The specifics differ
-
# between databases. In invalid cases, an error from the database is thrown.
-
1
def count(column_name = nil)
-
if block_given?
-
unless column_name.nil?
-
ActiveSupport::Deprecation.warn \
-
"When `count' is called with a block, it ignores other arguments. " \
-
"This behavior is now deprecated and will result in an ArgumentError in Rails 6.0."
-
end
-
-
return super()
-
end
-
-
calculate(:count, column_name)
-
end
-
-
# Calculates the average value on a given column. Returns +nil+ if there's
-
# no row. See #calculate for examples with options.
-
#
-
# Person.average(:age) # => 35.8
-
1
def average(column_name)
-
calculate(:average, column_name)
-
end
-
-
# Calculates the minimum value on a given column. The value is returned
-
# with the same data type of the column, or +nil+ if there's no row. See
-
# #calculate for examples with options.
-
#
-
# Person.minimum(:age) # => 7
-
1
def minimum(column_name)
-
calculate(:minimum, column_name)
-
end
-
-
# Calculates the maximum value on a given column. The value is returned
-
# with the same data type of the column, or +nil+ if there's no row. See
-
# #calculate for examples with options.
-
#
-
# Person.maximum(:age) # => 93
-
1
def maximum(column_name)
-
calculate(:maximum, column_name)
-
end
-
-
# Calculates the sum of values on a given column. The value is returned
-
# with the same data type of the column, +0+ if there's no row. See
-
# #calculate for examples with options.
-
#
-
# Person.sum(:age) # => 4562
-
1
def sum(column_name = nil)
-
if block_given?
-
unless column_name.nil?
-
ActiveSupport::Deprecation.warn \
-
"When `sum' is called with a block, it ignores other arguments. " \
-
"This behavior is now deprecated and will result in an ArgumentError in Rails 6.0."
-
end
-
-
return super()
-
end
-
-
calculate(:sum, column_name)
-
end
-
-
# This calculates aggregate values in the given column. Methods for #count, #sum, #average,
-
# #minimum, and #maximum have been added as shortcuts.
-
#
-
# Person.calculate(:count, :all) # The same as Person.count
-
# Person.average(:age) # SELECT AVG(age) FROM people...
-
#
-
# # Selects the minimum age for any family without any minors
-
# Person.group(:last_name).having("min(age) > 17").minimum(:age)
-
#
-
# Person.sum("2 * age")
-
#
-
# There are two basic forms of output:
-
#
-
# * Single aggregate value: The single value is type cast to Integer for COUNT, Float
-
# for AVG, and the given column's type for everything else.
-
#
-
# * Grouped values: This returns an ordered hash of the values and groups them. It
-
# takes either a column name, or the name of a belongs_to association.
-
#
-
# values = Person.group('last_name').maximum(:age)
-
# puts values["Drake"]
-
# # => 43
-
#
-
# drake = Family.find_by(last_name: 'Drake')
-
# values = Person.group(:family).maximum(:age) # Person belongs_to :family
-
# puts values[drake]
-
# # => 43
-
#
-
# values.each do |family, max_age|
-
# ...
-
# end
-
1
def calculate(operation, column_name)
-
if has_include?(column_name)
-
relation = apply_join_dependency
-
-
if operation.to_s.downcase == "count"
-
relation.distinct!
-
# PostgreSQL: ORDER BY expressions must appear in SELECT list when using DISTINCT
-
if (column_name == :all || column_name.nil?) && select_values.empty?
-
relation.order_values = []
-
end
-
end
-
-
relation.calculate(operation, column_name)
-
else
-
perform_calculation(operation, column_name)
-
end
-
end
-
-
# Use #pluck as a shortcut to select one or more attributes without
-
# loading a bunch of records just to grab the attributes you want.
-
#
-
# Person.pluck(:name)
-
#
-
# instead of
-
#
-
# Person.all.map(&:name)
-
#
-
# Pluck returns an Array of attribute values type-casted to match
-
# the plucked column names, if they can be deduced. Plucking an SQL fragment
-
# returns String values by default.
-
#
-
# Person.pluck(:name)
-
# # SELECT people.name FROM people
-
# # => ['David', 'Jeremy', 'Jose']
-
#
-
# Person.pluck(:id, :name)
-
# # SELECT people.id, people.name FROM people
-
# # => [[1, 'David'], [2, 'Jeremy'], [3, 'Jose']]
-
#
-
# Person.distinct.pluck(:role)
-
# # SELECT DISTINCT role FROM people
-
# # => ['admin', 'member', 'guest']
-
#
-
# Person.where(age: 21).limit(5).pluck(:id)
-
# # SELECT people.id FROM people WHERE people.age = 21 LIMIT 5
-
# # => [2, 3]
-
#
-
# Person.pluck('DATEDIFF(updated_at, created_at)')
-
# # SELECT DATEDIFF(updated_at, created_at) FROM people
-
# # => ['0', '27761', '173']
-
#
-
# See also #ids.
-
#
-
1
def pluck(*column_names)
-
2
if loaded? && (column_names.map(&:to_s) - @klass.attribute_names - @klass.attribute_aliases.keys).empty?
-
return records.pluck(*column_names)
-
end
-
-
2
if has_include?(column_names.first)
-
relation = apply_join_dependency
-
relation.pluck(*column_names)
-
else
-
2
klass.enforce_raw_sql_whitelist(column_names)
-
2
relation = spawn
-
2
relation.select_values = column_names
-
4
result = skip_query_cache_if_necessary { klass.connection.select_all(relation.arel, nil) }
-
2
result.cast_values(klass.attribute_types)
-
end
-
end
-
-
# Pluck all the ID's for the relation using the table's primary key
-
#
-
# Person.ids # SELECT people.id FROM people
-
# Person.joins(:companies).ids # SELECT people.id FROM people INNER JOIN companies ON companies.person_id = people.id
-
1
def ids
-
pluck primary_key
-
end
-
-
1
private
-
1
def has_include?(column_name)
-
2
eager_loading? || (includes_values.present? && column_name && column_name != :all)
-
end
-
-
1
def perform_calculation(operation, column_name)
-
operation = operation.to_s.downcase
-
-
# If #count is used with #distinct (i.e. `relation.distinct.count`) it is
-
# considered distinct.
-
distinct = distinct_value
-
-
if operation == "count"
-
column_name ||= select_for_count
-
if column_name == :all
-
if !distinct
-
distinct = distinct_select?(select_for_count) if group_values.empty?
-
elsif group_values.any? || select_values.empty? && order_values.empty?
-
column_name = primary_key
-
end
-
elsif distinct_select?(column_name)
-
distinct = nil
-
end
-
end
-
-
if group_values.any?
-
execute_grouped_calculation(operation, column_name, distinct)
-
else
-
execute_simple_calculation(operation, column_name, distinct)
-
end
-
end
-
-
1
def distinct_select?(column_name)
-
column_name.is_a?(::String) && /\bDISTINCT[\s(]/i.match?(column_name)
-
end
-
-
1
def aggregate_column(column_name)
-
return column_name if Arel::Expressions === column_name
-
-
if @klass.has_attribute?(column_name) || @klass.attribute_alias?(column_name)
-
@klass.arel_attribute(column_name)
-
else
-
Arel.sql(column_name == :all ? "*" : column_name.to_s)
-
end
-
end
-
-
1
def operation_over_aggregate_column(column, operation, distinct)
-
operation == "count" ? column.count(distinct) : column.send(operation)
-
end
-
-
1
def execute_simple_calculation(operation, column_name, distinct) #:nodoc:
-
column_alias = column_name
-
-
if operation == "count" && (column_name == :all && distinct || has_limit_or_offset?)
-
# Shortcut when limit is zero.
-
return 0 if limit_value == 0
-
-
query_builder = build_count_subquery(spawn, column_name, distinct)
-
else
-
# PostgreSQL doesn't like ORDER BY when there are no GROUP BY
-
relation = unscope(:order).distinct!(false)
-
-
column = aggregate_column(column_name)
-
-
select_value = operation_over_aggregate_column(column, operation, distinct)
-
if operation == "sum" && distinct
-
select_value.distinct = true
-
end
-
-
column_alias = select_value.alias
-
column_alias ||= @klass.connection.column_name_for_operation(operation, select_value)
-
relation.select_values = [select_value]
-
-
query_builder = relation.arel
-
end
-
-
result = skip_query_cache_if_necessary { @klass.connection.select_all(query_builder, nil) }
-
row = result.first
-
value = row && row.values.first
-
type = result.column_types.fetch(column_alias) do
-
type_for(column_name)
-
end
-
-
type_cast_calculated_value(value, type, operation)
-
end
-
-
1
def execute_grouped_calculation(operation, column_name, distinct) #:nodoc:
-
group_attrs = group_values
-
-
if group_attrs.first.respond_to?(:to_sym)
-
association = @klass._reflect_on_association(group_attrs.first)
-
associated = group_attrs.size == 1 && association && association.belongs_to? # only count belongs_to associations
-
group_fields = Array(associated ? association.foreign_key : group_attrs)
-
else
-
group_fields = group_attrs
-
end
-
group_fields = arel_columns(group_fields)
-
-
group_aliases = group_fields.map { |field| column_alias_for(field) }
-
group_columns = group_aliases.zip(group_fields)
-
-
if operation == "count" && column_name == :all
-
aggregate_alias = "count_all"
-
else
-
aggregate_alias = column_alias_for([operation, column_name].join(" "))
-
end
-
-
select_values = [
-
operation_over_aggregate_column(
-
aggregate_column(column_name),
-
operation,
-
distinct).as(aggregate_alias)
-
]
-
select_values += self.select_values unless having_clause.empty?
-
-
select_values.concat group_columns.map { |aliaz, field|
-
if field.respond_to?(:as)
-
field.as(aliaz)
-
else
-
"#{field} AS #{aliaz}"
-
end
-
}
-
-
relation = except(:group).distinct!(false)
-
relation.group_values = group_fields
-
relation.select_values = select_values
-
-
calculated_data = skip_query_cache_if_necessary { @klass.connection.select_all(relation.arel, nil) }
-
-
if association
-
key_ids = calculated_data.collect { |row| row[group_aliases.first] }
-
key_records = association.klass.base_class.where(association.klass.base_class.primary_key => key_ids)
-
key_records = Hash[key_records.map { |r| [r.id, r] }]
-
end
-
-
Hash[calculated_data.map do |row|
-
key = group_columns.map { |aliaz, col_name|
-
type = type_for(col_name) do
-
calculated_data.column_types.fetch(aliaz, Type.default_value)
-
end
-
type_cast_calculated_value(row[aliaz], type)
-
}
-
key = key.first if key.size == 1
-
key = key_records[key] if associated
-
-
type = calculated_data.column_types.fetch(aggregate_alias) { type_for(column_name) }
-
[key, type_cast_calculated_value(row[aggregate_alias], type, operation)]
-
end]
-
end
-
-
# Converts the given keys to the value that the database adapter returns as
-
# a usable column name:
-
#
-
# column_alias_for("users.id") # => "users_id"
-
# column_alias_for("sum(id)") # => "sum_id"
-
# column_alias_for("count(distinct users.id)") # => "count_distinct_users_id"
-
# column_alias_for("count(*)") # => "count_all"
-
1
def column_alias_for(keys)
-
if keys.respond_to? :name
-
keys = "#{keys.relation.name}.#{keys.name}"
-
end
-
-
table_name = keys.to_s.downcase
-
table_name.gsub!(/\*/, "all")
-
table_name.gsub!(/\W+/, " ")
-
table_name.strip!
-
table_name.gsub!(/ +/, "_")
-
-
@klass.connection.table_alias_for(table_name)
-
end
-
-
1
def type_for(field, &block)
-
field_name = field.respond_to?(:name) ? field.name.to_s : field.to_s.split(".").last
-
@klass.type_for_attribute(field_name, &block)
-
end
-
-
1
def type_cast_calculated_value(value, type, operation = nil)
-
case operation
-
when "count" then value.to_i
-
when "sum" then type.deserialize(value || 0)
-
when "average" then value && value.respond_to?(:to_d) ? value.to_d : value
-
else type.deserialize(value)
-
end
-
end
-
-
1
def select_for_count
-
if select_values.present?
-
return select_values.first if select_values.one?
-
select_values.join(", ")
-
else
-
:all
-
end
-
end
-
-
1
def build_count_subquery(relation, column_name, distinct)
-
if column_name == :all
-
relation.select_values = [ Arel.sql(FinderMethods::ONE_AS_ONE) ] unless distinct
-
else
-
column_alias = Arel.sql("count_column")
-
relation.select_values = [ aggregate_column(column_name).as(column_alias) ]
-
end
-
-
subquery = relation.arel.as(Arel.sql("subquery_for_count"))
-
select_value = operation_over_aggregate_column(column_alias || Arel.star, "count", false)
-
-
Arel::SelectManager.new(subquery).project(select_value)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_support/core_ext/string/filters"
-
-
1
module ActiveRecord
-
1
module FinderMethods
-
1
ONE_AS_ONE = "1 AS one"
-
-
# Find by id - This can either be a specific id (1), a list of ids (1, 5, 6), or an array of ids ([5, 6, 10]).
-
# If one or more records can not be found for the requested ids, then RecordNotFound will be raised. If the primary key
-
# is an integer, find by id coerces its arguments using +to_i+.
-
#
-
# Person.find(1) # returns the object for ID = 1
-
# Person.find("1") # returns the object for ID = 1
-
# Person.find("31-sarah") # returns the object for ID = 31
-
# Person.find(1, 2, 6) # returns an array for objects with IDs in (1, 2, 6)
-
# Person.find([7, 17]) # returns an array for objects with IDs in (7, 17)
-
# Person.find([1]) # returns an array for the object with ID = 1
-
# Person.where("administrator = 1").order("created_on DESC").find(1)
-
#
-
# NOTE: The returned records are in the same order as the ids you provide.
-
# If you want the results to be sorted by database, you can use ActiveRecord::QueryMethods#where
-
# method and provide an explicit ActiveRecord::QueryMethods#order option.
-
# But ActiveRecord::QueryMethods#where method doesn't raise ActiveRecord::RecordNotFound.
-
#
-
# ==== Find with lock
-
#
-
# Example for find with a lock: Imagine two concurrent transactions:
-
# each will read <tt>person.visits == 2</tt>, add 1 to it, and save, resulting
-
# in two saves of <tt>person.visits = 3</tt>. By locking the row, the second
-
# transaction has to wait until the first is finished; we get the
-
# expected <tt>person.visits == 4</tt>.
-
#
-
# Person.transaction do
-
# person = Person.lock(true).find(1)
-
# person.visits += 1
-
# person.save!
-
# end
-
#
-
# ==== Variations of #find
-
#
-
# Person.where(name: 'Spartacus', rating: 4)
-
# # returns a chainable list (which can be empty).
-
#
-
# Person.find_by(name: 'Spartacus', rating: 4)
-
# # returns the first item or nil.
-
#
-
# Person.find_or_initialize_by(name: 'Spartacus', rating: 4)
-
# # returns the first item or returns a new instance (requires you call .save to persist against the database).
-
#
-
# Person.find_or_create_by(name: 'Spartacus', rating: 4)
-
# # returns the first item or creates it and returns it.
-
#
-
# ==== Alternatives for #find
-
#
-
# Person.where(name: 'Spartacus', rating: 4).exists?(conditions = :none)
-
# # returns a boolean indicating if any record with the given conditions exist.
-
#
-
# Person.where(name: 'Spartacus', rating: 4).select("field1, field2, field3")
-
# # returns a chainable list of instances with only the mentioned fields.
-
#
-
# Person.where(name: 'Spartacus', rating: 4).ids
-
# # returns an Array of ids.
-
#
-
# Person.where(name: 'Spartacus', rating: 4).pluck(:field1, :field2)
-
# # returns an Array of the required fields.
-
1
def find(*args)
-
return super if block_given?
-
find_with_ids(*args)
-
end
-
-
# Finds the first record matching the specified conditions. There
-
# is no implied ordering so if order matters, you should specify it
-
# yourself.
-
#
-
# If no record is found, returns <tt>nil</tt>.
-
#
-
# Post.find_by name: 'Spartacus', rating: 4
-
# Post.find_by "published_at < ?", 2.weeks.ago
-
1
def find_by(arg, *args)
-
where(arg, *args).take
-
rescue ::RangeError
-
nil
-
end
-
-
# Like #find_by, except that if no record is found, raises
-
# an ActiveRecord::RecordNotFound error.
-
1
def find_by!(arg, *args)
-
where(arg, *args).take!
-
rescue ::RangeError
-
raise RecordNotFound.new("Couldn't find #{@klass.name} with an out of range value",
-
@klass.name, @klass.primary_key)
-
end
-
-
# Gives a record (or N records if a parameter is supplied) without any implied
-
# order. The order will depend on the database implementation.
-
# If an order is supplied it will be respected.
-
#
-
# Person.take # returns an object fetched by SELECT * FROM people LIMIT 1
-
# Person.take(5) # returns 5 objects fetched by SELECT * FROM people LIMIT 5
-
# Person.where(["name LIKE '%?'", name]).take
-
1
def take(limit = nil)
-
limit ? find_take_with_limit(limit) : find_take
-
end
-
-
# Same as #take but raises ActiveRecord::RecordNotFound if no record
-
# is found. Note that #take! accepts no arguments.
-
1
def take!
-
take || raise_record_not_found_exception!
-
end
-
-
# Find the first record (or first N records if a parameter is supplied).
-
# If no order is defined it will order by primary key.
-
#
-
# Person.first # returns the first object fetched by SELECT * FROM people ORDER BY people.id LIMIT 1
-
# Person.where(["user_name = ?", user_name]).first
-
# Person.where(["user_name = :u", { u: user_name }]).first
-
# Person.order("created_on DESC").offset(5).first
-
# Person.first(3) # returns the first three objects fetched by SELECT * FROM people ORDER BY people.id LIMIT 3
-
#
-
1
def first(limit = nil)
-
if limit
-
find_nth_with_limit(0, limit)
-
else
-
find_nth 0
-
end
-
end
-
-
# Same as #first but raises ActiveRecord::RecordNotFound if no record
-
# is found. Note that #first! accepts no arguments.
-
1
def first!
-
first || raise_record_not_found_exception!
-
end
-
-
# Find the last record (or last N records if a parameter is supplied).
-
# If no order is defined it will order by primary key.
-
#
-
# Person.last # returns the last object fetched by SELECT * FROM people
-
# Person.where(["user_name = ?", user_name]).last
-
# Person.order("created_on DESC").offset(5).last
-
# Person.last(3) # returns the last three objects fetched by SELECT * FROM people.
-
#
-
# Take note that in that last case, the results are sorted in ascending order:
-
#
-
# [#<Person id:2>, #<Person id:3>, #<Person id:4>]
-
#
-
# and not:
-
#
-
# [#<Person id:4>, #<Person id:3>, #<Person id:2>]
-
1
def last(limit = nil)
-
return find_last(limit) if loaded? || has_limit_or_offset?
-
-
result = ordered_relation.limit(limit)
-
result = result.reverse_order!
-
-
limit ? result.reverse : result.first
-
end
-
-
# Same as #last but raises ActiveRecord::RecordNotFound if no record
-
# is found. Note that #last! accepts no arguments.
-
1
def last!
-
last || raise_record_not_found_exception!
-
end
-
-
# Find the second record.
-
# If no order is defined it will order by primary key.
-
#
-
# Person.second # returns the second object fetched by SELECT * FROM people
-
# Person.offset(3).second # returns the second object from OFFSET 3 (which is OFFSET 4)
-
# Person.where(["user_name = :u", { u: user_name }]).second
-
1
def second
-
find_nth 1
-
end
-
-
# Same as #second but raises ActiveRecord::RecordNotFound if no record
-
# is found.
-
1
def second!
-
second || raise_record_not_found_exception!
-
end
-
-
# Find the third record.
-
# If no order is defined it will order by primary key.
-
#
-
# Person.third # returns the third object fetched by SELECT * FROM people
-
# Person.offset(3).third # returns the third object from OFFSET 3 (which is OFFSET 5)
-
# Person.where(["user_name = :u", { u: user_name }]).third
-
1
def third
-
find_nth 2
-
end
-
-
# Same as #third but raises ActiveRecord::RecordNotFound if no record
-
# is found.
-
1
def third!
-
third || raise_record_not_found_exception!
-
end
-
-
# Find the fourth record.
-
# If no order is defined it will order by primary key.
-
#
-
# Person.fourth # returns the fourth object fetched by SELECT * FROM people
-
# Person.offset(3).fourth # returns the fourth object from OFFSET 3 (which is OFFSET 6)
-
# Person.where(["user_name = :u", { u: user_name }]).fourth
-
1
def fourth
-
find_nth 3
-
end
-
-
# Same as #fourth but raises ActiveRecord::RecordNotFound if no record
-
# is found.
-
1
def fourth!
-
fourth || raise_record_not_found_exception!
-
end
-
-
# Find the fifth record.
-
# If no order is defined it will order by primary key.
-
#
-
# Person.fifth # returns the fifth object fetched by SELECT * FROM people
-
# Person.offset(3).fifth # returns the fifth object from OFFSET 3 (which is OFFSET 7)
-
# Person.where(["user_name = :u", { u: user_name }]).fifth
-
1
def fifth
-
find_nth 4
-
end
-
-
# Same as #fifth but raises ActiveRecord::RecordNotFound if no record
-
# is found.
-
1
def fifth!
-
fifth || raise_record_not_found_exception!
-
end
-
-
# Find the forty-second record. Also known as accessing "the reddit".
-
# If no order is defined it will order by primary key.
-
#
-
# Person.forty_two # returns the forty-second object fetched by SELECT * FROM people
-
# Person.offset(3).forty_two # returns the forty-second object from OFFSET 3 (which is OFFSET 44)
-
# Person.where(["user_name = :u", { u: user_name }]).forty_two
-
1
def forty_two
-
find_nth 41
-
end
-
-
# Same as #forty_two but raises ActiveRecord::RecordNotFound if no record
-
# is found.
-
1
def forty_two!
-
forty_two || raise_record_not_found_exception!
-
end
-
-
# Find the third-to-last record.
-
# If no order is defined it will order by primary key.
-
#
-
# Person.third_to_last # returns the third-to-last object fetched by SELECT * FROM people
-
# Person.offset(3).third_to_last # returns the third-to-last object from OFFSET 3
-
# Person.where(["user_name = :u", { u: user_name }]).third_to_last
-
1
def third_to_last
-
find_nth_from_last 3
-
end
-
-
# Same as #third_to_last but raises ActiveRecord::RecordNotFound if no record
-
# is found.
-
1
def third_to_last!
-
third_to_last || raise_record_not_found_exception!
-
end
-
-
# Find the second-to-last record.
-
# If no order is defined it will order by primary key.
-
#
-
# Person.second_to_last # returns the second-to-last object fetched by SELECT * FROM people
-
# Person.offset(3).second_to_last # returns the second-to-last object from OFFSET 3
-
# Person.where(["user_name = :u", { u: user_name }]).second_to_last
-
1
def second_to_last
-
find_nth_from_last 2
-
end
-
-
# Same as #second_to_last but raises ActiveRecord::RecordNotFound if no record
-
# is found.
-
1
def second_to_last!
-
second_to_last || raise_record_not_found_exception!
-
end
-
-
# Returns true if a record exists in the table that matches the +id+ or
-
# conditions given, or false otherwise. The argument can take six forms:
-
#
-
# * Integer - Finds the record with this primary key.
-
# * String - Finds the record with a primary key corresponding to this
-
# string (such as <tt>'5'</tt>).
-
# * Array - Finds the record that matches these +find+-style conditions
-
# (such as <tt>['name LIKE ?', "%#{query}%"]</tt>).
-
# * Hash - Finds the record that matches these +find+-style conditions
-
# (such as <tt>{name: 'David'}</tt>).
-
# * +false+ - Returns always +false+.
-
# * No args - Returns +false+ if the relation is empty, +true+ otherwise.
-
#
-
# For more information about specifying conditions as a hash or array,
-
# see the Conditions section in the introduction to ActiveRecord::Base.
-
#
-
# Note: You can't pass in a condition as a string (like <tt>name =
-
# 'Jamie'</tt>), since it would be sanitized and then queried against
-
# the primary key column, like <tt>id = 'name = \'Jamie\''</tt>.
-
#
-
# Person.exists?(5)
-
# Person.exists?('5')
-
# Person.exists?(['name LIKE ?', "%#{query}%"])
-
# Person.exists?(id: [1, 4, 8])
-
# Person.exists?(name: 'David')
-
# Person.exists?(false)
-
# Person.exists?
-
# Person.where(name: 'Spartacus', rating: 4).exists?
-
1
def exists?(conditions = :none)
-
if Base === conditions
-
raise ArgumentError, <<-MSG.squish
-
You are passing an instance of ActiveRecord::Base to `exists?`.
-
Please pass the id of the object by calling `.id`.
-
MSG
-
end
-
-
return false if !conditions || limit_value == 0
-
-
if eager_loading?
-
relation = apply_join_dependency(eager_loading: false)
-
return relation.exists?(conditions)
-
end
-
-
relation = construct_relation_for_exists(conditions)
-
-
skip_query_cache_if_necessary { connection.select_one(relation.arel, "#{name} Exists") } ? true : false
-
rescue ::RangeError
-
false
-
end
-
-
# This method is called whenever no records are found with either a single
-
# id or multiple ids and raises an ActiveRecord::RecordNotFound exception.
-
#
-
# The error message is different depending on whether a single id or
-
# multiple ids are provided. If multiple ids are provided, then the number
-
# of results obtained should be provided in the +result_size+ argument and
-
# the expected number of results should be provided in the +expected_size+
-
# argument.
-
1
def raise_record_not_found_exception!(ids = nil, result_size = nil, expected_size = nil, key = primary_key, not_found_ids = nil) # :nodoc:
-
conditions = arel.where_sql(@klass)
-
conditions = " [#{conditions}]" if conditions
-
name = @klass.name
-
-
if ids.nil?
-
error = "Couldn't find #{name}".dup
-
error << " with#{conditions}" if conditions
-
raise RecordNotFound.new(error, name, key)
-
elsif Array(ids).size == 1
-
error = "Couldn't find #{name} with '#{key}'=#{ids}#{conditions}"
-
raise RecordNotFound.new(error, name, key, ids)
-
else
-
error = "Couldn't find all #{name.pluralize} with '#{key}': ".dup
-
error << "(#{ids.join(", ")})#{conditions} (found #{result_size} results, but was looking for #{expected_size})."
-
error << " Couldn't find #{name.pluralize(not_found_ids.size)} with #{key.to_s.pluralize(not_found_ids.size)} #{not_found_ids.join(', ')}." if not_found_ids
-
raise RecordNotFound.new(error, name, key, ids)
-
end
-
end
-
-
1
private
-
-
1
def offset_index
-
offset_value || 0
-
end
-
-
1
def construct_relation_for_exists(conditions)
-
if distinct_value && offset_value
-
relation = limit(1)
-
else
-
relation = except(:select, :distinct, :order)._select!(ONE_AS_ONE).limit!(1)
-
end
-
-
case conditions
-
when Array, Hash
-
relation.where!(conditions) unless conditions.empty?
-
else
-
relation.where!(primary_key => conditions) unless conditions == :none
-
end
-
-
relation
-
end
-
-
1
def construct_join_dependency
-
including = eager_load_values + includes_values
-
ActiveRecord::Associations::JoinDependency.new(
-
klass, table, including
-
)
-
end
-
-
1
def apply_join_dependency(eager_loading: group_values.empty?)
-
join_dependency = construct_join_dependency
-
relation = except(:includes, :eager_load, :preload).joins!(join_dependency)
-
-
if eager_loading && !using_limitable_reflections?(join_dependency.reflections)
-
if has_limit_or_offset?
-
limited_ids = limited_ids_for(relation)
-
limited_ids.empty? ? relation.none! : relation.where!(primary_key => limited_ids)
-
end
-
relation.limit_value = relation.offset_value = nil
-
end
-
-
if block_given?
-
yield relation, join_dependency
-
else
-
relation
-
end
-
end
-
-
1
def limited_ids_for(relation)
-
values = @klass.connection.columns_for_distinct(
-
connection.column_name_from_arel_node(arel_attribute(primary_key)),
-
relation.order_values
-
)
-
-
relation = relation.except(:select).select(values).distinct!
-
-
id_rows = skip_query_cache_if_necessary { @klass.connection.select_all(relation.arel, "SQL") }
-
id_rows.map { |row| row[primary_key] }
-
end
-
-
1
def using_limitable_reflections?(reflections)
-
reflections.none?(&:collection?)
-
end
-
-
1
def find_with_ids(*ids)
-
raise UnknownPrimaryKey.new(@klass) if primary_key.nil?
-
-
expects_array = ids.first.kind_of?(Array)
-
return [] if expects_array && ids.first.empty?
-
-
ids = ids.flatten.compact.uniq
-
-
model_name = @klass.name
-
-
case ids.size
-
when 0
-
error_message = "Couldn't find #{model_name} without an ID"
-
raise RecordNotFound.new(error_message, model_name, primary_key)
-
when 1
-
result = find_one(ids.first)
-
expects_array ? [ result ] : result
-
else
-
find_some(ids)
-
end
-
rescue ::RangeError
-
error_message = "Couldn't find #{model_name} with an out of range ID"
-
raise RecordNotFound.new(error_message, model_name, primary_key, ids)
-
end
-
-
1
def find_one(id)
-
if ActiveRecord::Base === id
-
raise ArgumentError, <<-MSG.squish
-
You are passing an instance of ActiveRecord::Base to `find`.
-
Please pass the id of the object by calling `.id`.
-
MSG
-
end
-
-
relation = where(primary_key => id)
-
record = relation.take
-
-
raise_record_not_found_exception!(id, 0, 1) unless record
-
-
record
-
end
-
-
1
def find_some(ids)
-
return find_some_ordered(ids) unless order_values.present?
-
-
result = where(primary_key => ids).to_a
-
-
expected_size =
-
if limit_value && ids.size > limit_value
-
limit_value
-
else
-
ids.size
-
end
-
-
# 11 ids with limit 3, offset 9 should give 2 results.
-
if offset_value && (ids.size - offset_value < expected_size)
-
expected_size = ids.size - offset_value
-
end
-
-
if result.size == expected_size
-
result
-
else
-
raise_record_not_found_exception!(ids, result.size, expected_size)
-
end
-
end
-
-
1
def find_some_ordered(ids)
-
ids = ids.slice(offset_value || 0, limit_value || ids.size) || []
-
-
result = except(:limit, :offset).where(primary_key => ids).records
-
-
if result.size == ids.size
-
pk_type = @klass.type_for_attribute(primary_key)
-
-
records_by_id = result.index_by(&:id)
-
ids.map { |id| records_by_id.fetch(pk_type.cast(id)) }
-
else
-
raise_record_not_found_exception!(ids, result.size, ids.size)
-
end
-
end
-
-
1
def find_take
-
if loaded?
-
records.first
-
else
-
@take ||= limit(1).records.first
-
end
-
end
-
-
1
def find_take_with_limit(limit)
-
if loaded?
-
records.take(limit)
-
else
-
limit(limit).to_a
-
end
-
end
-
-
1
def find_nth(index)
-
@offsets[offset_index + index] ||= find_nth_with_limit(index, 1).first
-
end
-
-
1
def find_nth_with_limit(index, limit)
-
if loaded?
-
records[index, limit] || []
-
else
-
relation = ordered_relation
-
-
if limit_value
-
limit = [limit_value - index, limit].min
-
end
-
-
if limit > 0
-
relation = relation.offset(offset_index + index) unless index.zero?
-
relation.limit(limit).to_a
-
else
-
[]
-
end
-
end
-
end
-
-
1
def find_nth_from_last(index)
-
if loaded?
-
records[-index]
-
else
-
relation = ordered_relation
-
-
if equal?(relation) || has_limit_or_offset?
-
relation.records[-index]
-
else
-
relation.last(index)[-index]
-
end
-
end
-
end
-
-
1
def find_last(limit)
-
limit ? records.last(limit) : records.last
-
end
-
-
1
def ordered_relation
-
if order_values.empty? && primary_key
-
order(arel_attribute(primary_key).asc)
-
else
-
self
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActiveRecord
-
1
class Relation
-
1
class FromClause # :nodoc:
-
1
attr_reader :value, :name
-
-
1
def initialize(value, name)
-
1
@value = value
-
1
@name = name
-
end
-
-
1
def merge(other)
-
self
-
end
-
-
1
def empty?
-
2
value.nil?
-
end
-
-
1
def self.empty
-
1
@empty ||= new(nil, nil)
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_support/core_ext/hash/keys"
-
-
1
module ActiveRecord
-
1
class Relation
-
1
class HashMerger # :nodoc:
-
1
attr_reader :relation, :hash
-
-
1
def initialize(relation, hash)
-
hash.assert_valid_keys(*Relation::VALUE_METHODS)
-
-
@relation = relation
-
@hash = hash
-
end
-
-
1
def merge #:nodoc:
-
Merger.new(relation, other).merge
-
end
-
-
# Applying values to a relation has some side effects. E.g.
-
# interpolation might take place for where values. So we should
-
# build a relation to merge in rather than directly merging
-
# the values.
-
1
def other
-
other = Relation.create(
-
relation.klass,
-
table: relation.table,
-
predicate_builder: relation.predicate_builder
-
)
-
hash.each { |k, v|
-
if k == :joins
-
if Hash === v
-
other.joins!(v)
-
else
-
other.joins!(*v)
-
end
-
elsif k == :select
-
other._select!(v)
-
else
-
other.send("#{k}!", v)
-
end
-
}
-
other
-
end
-
end
-
-
1
class Merger # :nodoc:
-
1
attr_reader :relation, :values, :other
-
-
1
def initialize(relation, other)
-
@relation = relation
-
@values = other.values
-
@other = other
-
end
-
-
1
NORMAL_VALUES = Relation::VALUE_METHODS -
-
Relation::CLAUSE_METHODS -
-
[:includes, :preload, :joins, :left_outer_joins, :order, :reverse_order, :lock, :create_with, :reordering] # :nodoc:
-
-
1
def normal_values
-
NORMAL_VALUES
-
end
-
-
1
def merge
-
normal_values.each do |name|
-
value = values[name]
-
# The unless clause is here mostly for performance reasons (since the `send` call might be moderately
-
# expensive), most of the time the value is going to be `nil` or `.blank?`, the only catch is that
-
# `false.blank?` returns `true`, so there needs to be an extra check so that explicit `false` values
-
# don't fall through the cracks.
-
unless value.nil? || (value.blank? && false != value)
-
if name == :select
-
relation._select!(*value)
-
else
-
relation.send("#{name}!", *value)
-
end
-
end
-
end
-
-
merge_multi_values
-
merge_single_values
-
merge_clauses
-
merge_preloads
-
merge_joins
-
merge_outer_joins
-
-
relation
-
end
-
-
1
private
-
-
1
def merge_preloads
-
return if other.preload_values.empty? && other.includes_values.empty?
-
-
if other.klass == relation.klass
-
relation.preload!(*other.preload_values) unless other.preload_values.empty?
-
relation.includes!(other.includes_values) unless other.includes_values.empty?
-
else
-
reflection = relation.klass.reflect_on_all_associations.find do |r|
-
r.class_name == other.klass.name
-
end || return
-
-
unless other.preload_values.empty?
-
relation.preload! reflection.name => other.preload_values
-
end
-
-
unless other.includes_values.empty?
-
relation.includes! reflection.name => other.includes_values
-
end
-
end
-
end
-
-
1
def merge_joins
-
return if other.joins_values.blank?
-
-
if other.klass == relation.klass
-
relation.joins!(*other.joins_values)
-
else
-
joins_dependency = other.joins_values.map do |join|
-
case join
-
when Hash, Symbol, Array
-
ActiveRecord::Associations::JoinDependency.new(
-
other.klass, other.table, join
-
)
-
else
-
join
-
end
-
end
-
-
relation.joins!(*joins_dependency)
-
end
-
end
-
-
1
def merge_outer_joins
-
return if other.left_outer_joins_values.blank?
-
-
if other.klass == relation.klass
-
relation.left_outer_joins!(*other.left_outer_joins_values)
-
else
-
joins_dependency = other.left_outer_joins_values.map do |join|
-
case join
-
when Hash, Symbol, Array
-
ActiveRecord::Associations::JoinDependency.new(
-
other.klass, other.table, join
-
)
-
else
-
join
-
end
-
end
-
-
relation.left_outer_joins!(*joins_dependency)
-
end
-
end
-
-
1
def merge_multi_values
-
if other.reordering_value
-
# override any order specified in the original relation
-
relation.reorder!(*other.order_values)
-
elsif other.order_values.any?
-
# merge in order_values from relation
-
relation.order!(*other.order_values)
-
end
-
-
extensions = other.extensions - relation.extensions
-
relation.extending!(*extensions) if extensions.any?
-
end
-
-
1
def merge_single_values
-
relation.lock_value ||= other.lock_value if other.lock_value
-
-
unless other.create_with_value.blank?
-
relation.create_with_value = (relation.create_with_value || {}).merge(other.create_with_value)
-
end
-
end
-
-
1
def merge_clauses
-
relation.from_clause = other.from_clause if replace_from_clause?
-
-
where_clause = relation.where_clause.merge(other.where_clause)
-
relation.where_clause = where_clause unless where_clause.empty?
-
-
having_clause = relation.having_clause.merge(other.having_clause)
-
relation.having_clause = having_clause unless having_clause.empty?
-
end
-
-
1
def replace_from_clause?
-
relation.from_clause.empty? && !other.from_clause.empty? &&
-
relation.klass.base_class == other.klass.base_class
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActiveRecord
-
1
class PredicateBuilder # :nodoc:
-
1
delegate :resolve_column_aliases, to: :table
-
-
1
def initialize(table)
-
1
@table = table
-
1
@handlers = []
-
-
1
register_handler(BasicObject, BasicObjectHandler.new(self))
-
1
register_handler(Base, BaseHandler.new(self))
-
1
register_handler(Range, RangeHandler.new(self))
-
1
register_handler(Relation, RelationHandler.new)
-
1
register_handler(Array, ArrayHandler.new(self))
-
1
register_handler(Set, ArrayHandler.new(self))
-
end
-
-
1
def build_from_hash(attributes)
-
attributes = convert_dot_notation_to_hash(attributes)
-
expand_from_hash(attributes)
-
end
-
-
1
def self.references(attributes)
-
attributes.map do |key, value|
-
if value.is_a?(Hash)
-
key
-
else
-
key = key.to_s
-
key.split(".".freeze).first if key.include?(".".freeze)
-
end
-
end.compact
-
end
-
-
# Define how a class is converted to Arel nodes when passed to +where+.
-
# The handler can be any object that responds to +call+, and will be used
-
# for any value that +===+ the class given. For example:
-
#
-
# MyCustomDateRange = Struct.new(:start, :end)
-
# handler = proc do |column, range|
-
# Arel::Nodes::Between.new(column,
-
# Arel::Nodes::And.new([range.start, range.end])
-
# )
-
# end
-
# ActiveRecord::PredicateBuilder.new("users").register_handler(MyCustomDateRange, handler)
-
1
def register_handler(klass, handler)
-
6
@handlers.unshift([klass, handler])
-
end
-
-
1
def build(attribute, value)
-
if table.type(attribute.name).force_equality?(value)
-
bind = build_bind_attribute(attribute.name, value)
-
attribute.eq(bind)
-
else
-
handler_for(value).call(attribute, value)
-
end
-
end
-
-
1
def build_bind_attribute(column_name, value)
-
attr = Relation::QueryAttribute.new(column_name.to_s, value, table.type(column_name))
-
Arel::Nodes::BindParam.new(attr)
-
end
-
-
1
protected
-
-
1
attr_reader :table
-
-
1
def expand_from_hash(attributes)
-
return ["1=0"] if attributes.empty?
-
-
attributes.flat_map do |key, value|
-
if value.is_a?(Hash) && !table.has_column?(key)
-
associated_predicate_builder(key).expand_from_hash(value)
-
elsif table.associated_with?(key)
-
# Find the foreign key when using queries such as:
-
# Post.where(author: author)
-
#
-
# For polymorphic relationships, find the foreign key and type:
-
# PriceEstimate.where(estimate_of: treasure)
-
associated_table = table.associated_table(key)
-
if associated_table.polymorphic_association?
-
case value.is_a?(Array) ? value.first : value
-
when Base, Relation
-
value = [value] unless value.is_a?(Array)
-
klass = PolymorphicArrayValue
-
end
-
end
-
-
klass ||= AssociationQueryValue
-
queries = klass.new(associated_table, value).queries.map do |query|
-
expand_from_hash(query).reduce(&:and)
-
end
-
queries.reduce(&:or)
-
elsif table.aggregated_with?(key)
-
mapping = table.reflect_on_aggregation(key).mapping
-
values = value.nil? ? [nil] : Array.wrap(value)
-
if mapping.length == 1 || values.empty?
-
column_name, aggr_attr = mapping.first
-
values = values.map do |object|
-
object.respond_to?(aggr_attr) ? object.public_send(aggr_attr) : object
-
end
-
build(table.arel_attribute(column_name), values)
-
else
-
queries = values.map do |object|
-
mapping.map do |field_attr, aggregate_attr|
-
build(table.arel_attribute(field_attr), object.try!(aggregate_attr))
-
end.reduce(&:and)
-
end
-
queries.reduce(&:or)
-
end
-
else
-
build(table.arel_attribute(key), value)
-
end
-
end
-
end
-
-
1
private
-
-
1
def associated_predicate_builder(association_name)
-
self.class.new(table.associated_table(association_name))
-
end
-
-
1
def convert_dot_notation_to_hash(attributes)
-
dot_notation = attributes.select do |k, v|
-
k.include?(".".freeze) && !v.is_a?(Hash)
-
end
-
-
dot_notation.each_key do |key|
-
table_name, column_name = key.split(".".freeze)
-
value = attributes.delete(key)
-
attributes[table_name] ||= {}
-
-
attributes[table_name] = attributes[table_name].merge(column_name => value)
-
end
-
-
attributes
-
end
-
-
1
def handler_for(object)
-
@handlers.detect { |klass, _| klass === object }.last
-
end
-
end
-
end
-
-
1
require "active_record/relation/predicate_builder/array_handler"
-
1
require "active_record/relation/predicate_builder/base_handler"
-
1
require "active_record/relation/predicate_builder/basic_object_handler"
-
1
require "active_record/relation/predicate_builder/range_handler"
-
1
require "active_record/relation/predicate_builder/relation_handler"
-
-
1
require "active_record/relation/predicate_builder/association_query_value"
-
1
require "active_record/relation/predicate_builder/polymorphic_array_value"
-
# frozen_string_literal: true
-
-
1
module ActiveRecord
-
1
class PredicateBuilder
-
1
class ArrayHandler # :nodoc:
-
1
def initialize(predicate_builder)
-
2
@predicate_builder = predicate_builder
-
end
-
-
1
def call(attribute, value)
-
return attribute.in([]) if value.empty?
-
-
values = value.map { |x| x.is_a?(Base) ? x.id : x }
-
nils, values = values.partition(&:nil?)
-
ranges, values = values.partition { |v| v.is_a?(Range) }
-
-
values_predicate =
-
case values.length
-
when 0 then NullPredicate
-
when 1 then predicate_builder.build(attribute, values.first)
-
else
-
values.map! do |v|
-
predicate_builder.build_bind_attribute(attribute.name, v)
-
end
-
values.empty? ? NullPredicate : attribute.in(values)
-
end
-
-
unless nils.empty?
-
values_predicate = values_predicate.or(predicate_builder.build(attribute, nil))
-
end
-
-
array_predicates = ranges.map { |range| predicate_builder.build(attribute, range) }
-
array_predicates.unshift(values_predicate)
-
array_predicates.inject(&:or)
-
end
-
-
1
protected
-
-
1
attr_reader :predicate_builder
-
-
1
module NullPredicate # :nodoc:
-
1
def self.or(other)
-
other
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActiveRecord
-
1
class PredicateBuilder
-
1
class AssociationQueryValue # :nodoc:
-
1
def initialize(associated_table, value)
-
@associated_table = associated_table
-
@value = value
-
end
-
-
1
def queries
-
[associated_table.association_join_foreign_key.to_s => ids]
-
end
-
-
# TODO Change this to private once we've dropped Ruby 2.2 support.
-
# Workaround for Ruby 2.2 "private attribute?" warning.
-
1
protected
-
1
attr_reader :associated_table, :value
-
-
1
private
-
1
def ids
-
case value
-
when Relation
-
value.select_values.empty? ? value.select(primary_key) : value
-
when Array
-
value.map { |v| convert_to_id(v) }
-
else
-
convert_to_id(value)
-
end
-
end
-
-
1
def primary_key
-
associated_table.association_join_primary_key
-
end
-
-
1
def convert_to_id(value)
-
case value
-
when Base
-
value._read_attribute(primary_key)
-
else
-
value
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActiveRecord
-
1
class PredicateBuilder
-
1
class BaseHandler # :nodoc:
-
1
def initialize(predicate_builder)
-
1
@predicate_builder = predicate_builder
-
end
-
-
1
def call(attribute, value)
-
predicate_builder.build(attribute, value.id)
-
end
-
-
1
protected
-
-
1
attr_reader :predicate_builder
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActiveRecord
-
1
class PredicateBuilder
-
1
class BasicObjectHandler # :nodoc:
-
1
def initialize(predicate_builder)
-
1
@predicate_builder = predicate_builder
-
end
-
-
1
def call(attribute, value)
-
bind = predicate_builder.build_bind_attribute(attribute.name, value)
-
attribute.eq(bind)
-
end
-
-
1
protected
-
-
1
attr_reader :predicate_builder
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActiveRecord
-
1
class PredicateBuilder
-
1
class PolymorphicArrayValue # :nodoc:
-
1
def initialize(associated_table, values)
-
@associated_table = associated_table
-
@values = values
-
end
-
-
1
def queries
-
type_to_ids_mapping.map do |type, ids|
-
{
-
associated_table.association_foreign_type.to_s => type,
-
associated_table.association_foreign_key.to_s => ids
-
}
-
end
-
end
-
-
# TODO Change this to private once we've dropped Ruby 2.2 support.
-
# Workaround for Ruby 2.2 "private attribute?" warning.
-
1
protected
-
1
attr_reader :associated_table, :values
-
-
1
private
-
1
def type_to_ids_mapping
-
default_hash = Hash.new { |hsh, key| hsh[key] = [] }
-
values.each_with_object(default_hash) do |value, hash|
-
hash[klass(value).polymorphic_name] << convert_to_id(value)
-
end
-
end
-
-
1
def primary_key(value)
-
associated_table.association_join_primary_key(klass(value))
-
end
-
-
1
def klass(value)
-
case value
-
when Base
-
value.class
-
when Relation
-
value.klass
-
end
-
end
-
-
1
def convert_to_id(value)
-
case value
-
when Base
-
value._read_attribute(primary_key(value))
-
when Relation
-
value.select(primary_key(value))
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActiveRecord
-
1
class PredicateBuilder
-
1
class RangeHandler # :nodoc:
-
1
class RangeWithBinds < Struct.new(:begin, :end)
-
1
def exclude_end?
-
false
-
end
-
end
-
-
1
def initialize(predicate_builder)
-
1
@predicate_builder = predicate_builder
-
end
-
-
1
def call(attribute, value)
-
begin_bind = predicate_builder.build_bind_attribute(attribute.name, value.begin)
-
end_bind = predicate_builder.build_bind_attribute(attribute.name, value.end)
-
-
if begin_bind.value.infinity?
-
if end_bind.value.infinity?
-
attribute.not_in([])
-
elsif value.exclude_end?
-
attribute.lt(end_bind)
-
else
-
attribute.lteq(end_bind)
-
end
-
elsif end_bind.value.infinity?
-
attribute.gteq(begin_bind)
-
elsif value.exclude_end?
-
attribute.gteq(begin_bind).and(attribute.lt(end_bind))
-
else
-
attribute.between(RangeWithBinds.new(begin_bind, end_bind))
-
end
-
end
-
-
1
protected
-
-
1
attr_reader :predicate_builder
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActiveRecord
-
1
class PredicateBuilder
-
1
class RelationHandler # :nodoc:
-
1
def call(attribute, value)
-
if value.eager_loading?
-
value = value.send(:apply_join_dependency)
-
end
-
-
if value.select_values.empty?
-
value = value.select(value.arel_attribute(value.klass.primary_key))
-
end
-
-
attribute.in(value.arel)
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_model/attribute"
-
-
1
module ActiveRecord
-
1
class Relation
-
1
class QueryAttribute < ActiveModel::Attribute # :nodoc:
-
1
def type_cast(value)
-
value
-
end
-
-
1
def value_for_database
-
@value_for_database ||= super
-
end
-
-
1
def with_cast_value(value)
-
QueryAttribute.new(name, value, type)
-
end
-
-
1
def nil?
-
unless value_before_type_cast.is_a?(StatementCache::Substitute)
-
value_before_type_cast.nil? ||
-
type.respond_to?(:subtype, true) && value_for_database.nil?
-
end
-
end
-
-
1
def boundable?
-
return @_boundable if defined?(@_boundable)
-
value_for_database unless value_before_type_cast.is_a?(StatementCache::Substitute)
-
@_boundable = true
-
rescue ::RangeError
-
@_boundable = false
-
end
-
-
1
def infinity?
-
_infinity?(value_before_type_cast) || boundable? && _infinity?(value_for_database)
-
end
-
-
1
private
-
1
def _infinity?(value)
-
value.respond_to?(:infinite?) && value.infinite?
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_record/relation/from_clause"
-
1
require "active_record/relation/query_attribute"
-
1
require "active_record/relation/where_clause"
-
1
require "active_record/relation/where_clause_factory"
-
1
require "active_model/forbidden_attributes_protection"
-
-
1
module ActiveRecord
-
1
module QueryMethods
-
1
extend ActiveSupport::Concern
-
-
1
include ActiveModel::ForbiddenAttributesProtection
-
-
# WhereChain objects act as placeholder for queries in which #where does not have any parameter.
-
# In this case, #where must be chained with #not to return a new relation.
-
1
class WhereChain
-
1
include ActiveModel::ForbiddenAttributesProtection
-
-
1
def initialize(scope)
-
@scope = scope
-
end
-
-
# Returns a new relation expressing WHERE + NOT condition according to
-
# the conditions in the arguments.
-
#
-
# #not accepts conditions as a string, array, or hash. See QueryMethods#where for
-
# more details on each format.
-
#
-
# User.where.not("name = 'Jon'")
-
# # SELECT * FROM users WHERE NOT (name = 'Jon')
-
#
-
# User.where.not(["name = ?", "Jon"])
-
# # SELECT * FROM users WHERE NOT (name = 'Jon')
-
#
-
# User.where.not(name: "Jon")
-
# # SELECT * FROM users WHERE name != 'Jon'
-
#
-
# User.where.not(name: nil)
-
# # SELECT * FROM users WHERE name IS NOT NULL
-
#
-
# User.where.not(name: %w(Ko1 Nobu))
-
# # SELECT * FROM users WHERE name NOT IN ('Ko1', 'Nobu')
-
#
-
# User.where.not(name: "Jon", role: "admin")
-
# # SELECT * FROM users WHERE name != 'Jon' AND role != 'admin'
-
1
def not(opts, *rest)
-
opts = sanitize_forbidden_attributes(opts)
-
-
where_clause = @scope.send(:where_clause_factory).build(opts, rest)
-
-
@scope.references!(PredicateBuilder.references(opts)) if Hash === opts
-
@scope.where_clause += where_clause.invert
-
@scope
-
end
-
end
-
-
1
FROZEN_EMPTY_ARRAY = [].freeze
-
1
FROZEN_EMPTY_HASH = {}.freeze
-
-
1
Relation::VALUE_METHODS.each do |name|
-
23
method_name = \
-
case name
-
11
when *Relation::MULTI_VALUE_METHODS then "#{name}_values"
-
9
when *Relation::SINGLE_VALUE_METHODS then "#{name}_value"
-
3
when *Relation::CLAUSE_METHODS then "#{name}_clause"
-
end
-
23
class_eval <<-CODE, __FILE__, __LINE__ + 1
-
23
def #{method_name} # def includes_values
-
23
get_value(#{name.inspect}) # get_value(:includes)
-
end # end
-
-
23
def #{method_name}=(value) # def includes_values=(value)
-
23
set_value(#{name.inspect}, value) # set_value(:includes, value)
-
end # end
-
CODE
-
end
-
-
1
alias extensions extending_values
-
-
# Specify relationships to be included in the result set. For
-
# example:
-
#
-
# users = User.includes(:address)
-
# users.each do |user|
-
# user.address.city
-
# end
-
#
-
# allows you to access the +address+ attribute of the +User+ model without
-
# firing an additional query. This will often result in a
-
# performance improvement over a simple join.
-
#
-
# You can also specify multiple relationships, like this:
-
#
-
# users = User.includes(:address, :friends)
-
#
-
# Loading nested relationships is possible using a Hash:
-
#
-
# users = User.includes(:address, friends: [:address, :followers])
-
#
-
# === conditions
-
#
-
# If you want to add conditions to your included models you'll have
-
# to explicitly reference them. For example:
-
#
-
# User.includes(:posts).where('posts.name = ?', 'example')
-
#
-
# Will throw an error, but this will work:
-
#
-
# User.includes(:posts).where('posts.name = ?', 'example').references(:posts)
-
#
-
# Note that #includes works with association names while #references needs
-
# the actual table name.
-
1
def includes(*args)
-
check_if_method_has_arguments!(:includes, args)
-
spawn.includes!(*args)
-
end
-
-
1
def includes!(*args) # :nodoc:
-
args.reject!(&:blank?)
-
args.flatten!
-
-
self.includes_values |= args
-
self
-
end
-
-
# Forces eager loading by performing a LEFT OUTER JOIN on +args+:
-
#
-
# User.eager_load(:posts)
-
# # SELECT "users"."id" AS t0_r0, "users"."name" AS t0_r1, ...
-
# # FROM "users" LEFT OUTER JOIN "posts" ON "posts"."user_id" =
-
# # "users"."id"
-
1
def eager_load(*args)
-
check_if_method_has_arguments!(:eager_load, args)
-
spawn.eager_load!(*args)
-
end
-
-
1
def eager_load!(*args) # :nodoc:
-
self.eager_load_values += args
-
self
-
end
-
-
# Allows preloading of +args+, in the same way that #includes does:
-
#
-
# User.preload(:posts)
-
# # SELECT "posts".* FROM "posts" WHERE "posts"."user_id" IN (1, 2, 3)
-
1
def preload(*args)
-
check_if_method_has_arguments!(:preload, args)
-
spawn.preload!(*args)
-
end
-
-
1
def preload!(*args) # :nodoc:
-
self.preload_values += args
-
self
-
end
-
-
# Use to indicate that the given +table_names+ are referenced by an SQL string,
-
# and should therefore be JOINed in any query rather than loaded separately.
-
# This method only works in conjunction with #includes.
-
# See #includes for more details.
-
#
-
# User.includes(:posts).where("posts.name = 'foo'")
-
# # Doesn't JOIN the posts table, resulting in an error.
-
#
-
# User.includes(:posts).where("posts.name = 'foo'").references(:posts)
-
# # Query now knows the string references posts, so adds a JOIN
-
1
def references(*table_names)
-
check_if_method_has_arguments!(:references, table_names)
-
spawn.references!(*table_names)
-
end
-
-
1
def references!(*table_names) # :nodoc:
-
table_names.flatten!
-
table_names.map!(&:to_s)
-
-
self.references_values |= table_names
-
self
-
end
-
-
# Works in two unique ways.
-
#
-
# First: takes a block so it can be used just like <tt>Array#select</tt>.
-
#
-
# Model.all.select { |m| m.field == value }
-
#
-
# This will build an array of objects from the database for the scope,
-
# converting them into an array and iterating through them using
-
# <tt>Array#select</tt>.
-
#
-
# Second: Modifies the SELECT statement for the query so that only certain
-
# fields are retrieved:
-
#
-
# Model.select(:field)
-
# # => [#<Model id: nil, field: "value">]
-
#
-
# Although in the above example it looks as though this method returns an
-
# array, it actually returns a relation object and can have other query
-
# methods appended to it, such as the other methods in ActiveRecord::QueryMethods.
-
#
-
# The argument to the method can also be an array of fields.
-
#
-
# Model.select(:field, :other_field, :and_one_more)
-
# # => [#<Model id: nil, field: "value", other_field: "value", and_one_more: "value">]
-
#
-
# You can also use one or more strings, which will be used unchanged as SELECT fields.
-
#
-
# Model.select('field AS field_one', 'other_field AS field_two')
-
# # => [#<Model id: nil, field: "value", other_field: "value">]
-
#
-
# If an alias was specified, it will be accessible from the resulting objects:
-
#
-
# Model.select('field AS field_one').first.field_one
-
# # => "value"
-
#
-
# Accessing attributes of an object that do not have fields retrieved by a select
-
# except +id+ will throw ActiveModel::MissingAttributeError:
-
#
-
# Model.select(:field).first.other_field
-
# # => ActiveModel::MissingAttributeError: missing attribute: other_field
-
1
def select(*fields)
-
if block_given?
-
if fields.any?
-
raise ArgumentError, "`select' with block doesn't take arguments."
-
end
-
-
return super()
-
end
-
-
raise ArgumentError, "Call `select' with at least one field" if fields.empty?
-
spawn._select!(*fields)
-
end
-
-
1
def _select!(*fields) # :nodoc:
-
fields.flatten!
-
fields.map! do |field|
-
klass.attribute_alias?(field) ? klass.attribute_alias(field).to_sym : field
-
end
-
self.select_values += fields
-
self
-
end
-
-
# Allows to specify a group attribute:
-
#
-
# User.group(:name)
-
# # SELECT "users".* FROM "users" GROUP BY name
-
#
-
# Returns an array with distinct records based on the +group+ attribute:
-
#
-
# User.select([:id, :name])
-
# # => [#<User id: 1, name: "Oscar">, #<User id: 2, name: "Oscar">, #<User id: 3, name: "Foo">]
-
#
-
# User.group(:name)
-
# # => [#<User id: 3, name: "Foo", ...>, #<User id: 2, name: "Oscar", ...>]
-
#
-
# User.group('name AS grouped_name, age')
-
# # => [#<User id: 3, name: "Foo", age: 21, ...>, #<User id: 2, name: "Oscar", age: 21, ...>, #<User id: 5, name: "Foo", age: 23, ...>]
-
#
-
# Passing in an array of attributes to group by is also supported.
-
#
-
# User.select([:id, :first_name]).group(:id, :first_name).first(3)
-
# # => [#<User id: 1, first_name: "Bill">, #<User id: 2, first_name: "Earl">, #<User id: 3, first_name: "Beto">]
-
1
def group(*args)
-
check_if_method_has_arguments!(:group, args)
-
spawn.group!(*args)
-
end
-
-
1
def group!(*args) # :nodoc:
-
args.flatten!
-
-
self.group_values += args
-
self
-
end
-
-
# Allows to specify an order attribute:
-
#
-
# User.order(:name)
-
# # SELECT "users".* FROM "users" ORDER BY "users"."name" ASC
-
#
-
# User.order(email: :desc)
-
# # SELECT "users".* FROM "users" ORDER BY "users"."email" DESC
-
#
-
# User.order(:name, email: :desc)
-
# # SELECT "users".* FROM "users" ORDER BY "users"."name" ASC, "users"."email" DESC
-
#
-
# User.order('name')
-
# # SELECT "users".* FROM "users" ORDER BY name
-
#
-
# User.order('name DESC')
-
# # SELECT "users".* FROM "users" ORDER BY name DESC
-
#
-
# User.order('name DESC, email')
-
# # SELECT "users".* FROM "users" ORDER BY name DESC, email
-
1
def order(*args)
-
2
check_if_method_has_arguments!(:order, args)
-
2
spawn.order!(*args)
-
end
-
-
# Same as #order but operates on relation in-place instead of copying.
-
1
def order!(*args) # :nodoc:
-
2
preprocess_order_args(args)
-
-
2
self.order_values += args
-
2
self
-
end
-
-
# Replaces any existing order defined on the relation with the specified order.
-
#
-
# User.order('email DESC').reorder('id ASC') # generated SQL has 'ORDER BY id ASC'
-
#
-
# Subsequent calls to order on the same relation will be appended. For example:
-
#
-
# User.order('email DESC').reorder('id ASC').order('name ASC')
-
#
-
# generates a query with 'ORDER BY id ASC, name ASC'.
-
1
def reorder(*args)
-
check_if_method_has_arguments!(:reorder, args)
-
spawn.reorder!(*args)
-
end
-
-
# Same as #reorder but operates on relation in-place instead of copying.
-
1
def reorder!(*args) # :nodoc:
-
preprocess_order_args(args)
-
-
self.reordering_value = true
-
self.order_values = args
-
self
-
end
-
-
1
VALID_UNSCOPING_VALUES = Set.new([:where, :select, :group, :order, :lock,
-
:limit, :offset, :joins, :left_outer_joins,
-
:includes, :from, :readonly, :having])
-
-
# Removes an unwanted relation that is already defined on a chain of relations.
-
# This is useful when passing around chains of relations and would like to
-
# modify the relations without reconstructing the entire chain.
-
#
-
# User.order('email DESC').unscope(:order) == User.all
-
#
-
# The method arguments are symbols which correspond to the names of the methods
-
# which should be unscoped. The valid arguments are given in VALID_UNSCOPING_VALUES.
-
# The method can also be called with multiple arguments. For example:
-
#
-
# User.order('email DESC').select('id').where(name: "John")
-
# .unscope(:order, :select, :where) == User.all
-
#
-
# One can additionally pass a hash as an argument to unscope specific +:where+ values.
-
# This is done by passing a hash with a single key-value pair. The key should be
-
# +:where+ and the value should be the where value to unscope. For example:
-
#
-
# User.where(name: "John", active: true).unscope(where: :name)
-
# == User.where(active: true)
-
#
-
# This method is similar to #except, but unlike
-
# #except, it persists across merges:
-
#
-
# User.order('email').merge(User.except(:order))
-
# == User.order('email')
-
#
-
# User.order('email').merge(User.unscope(:order))
-
# == User.all
-
#
-
# This means it can be used in association definitions:
-
#
-
# has_many :comments, -> { unscope(where: :trashed) }
-
#
-
1
def unscope(*args)
-
check_if_method_has_arguments!(:unscope, args)
-
spawn.unscope!(*args)
-
end
-
-
1
def unscope!(*args) # :nodoc:
-
args.flatten!
-
self.unscope_values += args
-
-
args.each do |scope|
-
case scope
-
when Symbol
-
scope = :left_outer_joins if scope == :left_joins
-
if !VALID_UNSCOPING_VALUES.include?(scope)
-
raise ArgumentError, "Called unscope() with invalid unscoping argument ':#{scope}'. Valid arguments are :#{VALID_UNSCOPING_VALUES.to_a.join(", :")}."
-
end
-
set_value(scope, DEFAULT_VALUES[scope])
-
when Hash
-
scope.each do |key, target_value|
-
if key != :where
-
raise ArgumentError, "Hash arguments in .unscope(*args) must have :where as the key."
-
end
-
-
target_values = Array(target_value).map(&:to_s)
-
self.where_clause = where_clause.except(*target_values)
-
end
-
else
-
raise ArgumentError, "Unrecognized scoping: #{args.inspect}. Use .unscope(where: :attribute_name) or .unscope(:order), for example."
-
end
-
end
-
-
self
-
end
-
-
# Performs a joins on +args+. The given symbol(s) should match the name of
-
# the association(s).
-
#
-
# User.joins(:posts)
-
# # SELECT "users".*
-
# # FROM "users"
-
# # INNER JOIN "posts" ON "posts"."user_id" = "users"."id"
-
#
-
# Multiple joins:
-
#
-
# User.joins(:posts, :account)
-
# # SELECT "users".*
-
# # FROM "users"
-
# # INNER JOIN "posts" ON "posts"."user_id" = "users"."id"
-
# # INNER JOIN "accounts" ON "accounts"."id" = "users"."account_id"
-
#
-
# Nested joins:
-
#
-
# User.joins(posts: [:comments])
-
# # SELECT "users".*
-
# # FROM "users"
-
# # INNER JOIN "posts" ON "posts"."user_id" = "users"."id"
-
# # INNER JOIN "comments" "comments_posts"
-
# # ON "comments_posts"."post_id" = "posts"."id"
-
#
-
# You can use strings in order to customize your joins:
-
#
-
# User.joins("LEFT JOIN bookmarks ON bookmarks.bookmarkable_type = 'Post' AND bookmarks.user_id = users.id")
-
# # SELECT "users".* FROM "users" LEFT JOIN bookmarks ON bookmarks.bookmarkable_type = 'Post' AND bookmarks.user_id = users.id
-
1
def joins(*args)
-
check_if_method_has_arguments!(:joins, args)
-
spawn.joins!(*args)
-
end
-
-
1
def joins!(*args) # :nodoc:
-
args.compact!
-
args.flatten!
-
self.joins_values += args
-
self
-
end
-
-
# Performs a left outer joins on +args+:
-
#
-
# User.left_outer_joins(:posts)
-
# => SELECT "users".* FROM "users" LEFT OUTER JOIN "posts" ON "posts"."user_id" = "users"."id"
-
#
-
1
def left_outer_joins(*args)
-
check_if_method_has_arguments!(__callee__, args)
-
spawn.left_outer_joins!(*args)
-
end
-
1
alias :left_joins :left_outer_joins
-
-
1
def left_outer_joins!(*args) # :nodoc:
-
args.compact!
-
args.flatten!
-
self.left_outer_joins_values += args
-
self
-
end
-
-
# Returns a new relation, which is the result of filtering the current relation
-
# according to the conditions in the arguments.
-
#
-
# #where accepts conditions in one of several formats. In the examples below, the resulting
-
# SQL is given as an illustration; the actual query generated may be different depending
-
# on the database adapter.
-
#
-
# === string
-
#
-
# A single string, without additional arguments, is passed to the query
-
# constructor as an SQL fragment, and used in the where clause of the query.
-
#
-
# Client.where("orders_count = '2'")
-
# # SELECT * from clients where orders_count = '2';
-
#
-
# Note that building your own string from user input may expose your application
-
# to injection attacks if not done properly. As an alternative, it is recommended
-
# to use one of the following methods.
-
#
-
# === array
-
#
-
# If an array is passed, then the first element of the array is treated as a template, and
-
# the remaining elements are inserted into the template to generate the condition.
-
# Active Record takes care of building the query to avoid injection attacks, and will
-
# convert from the ruby type to the database type where needed. Elements are inserted
-
# into the string in the order in which they appear.
-
#
-
# User.where(["name = ? and email = ?", "Joe", "joe@example.com"])
-
# # SELECT * FROM users WHERE name = 'Joe' AND email = 'joe@example.com';
-
#
-
# Alternatively, you can use named placeholders in the template, and pass a hash as the
-
# second element of the array. The names in the template are replaced with the corresponding
-
# values from the hash.
-
#
-
# User.where(["name = :name and email = :email", { name: "Joe", email: "joe@example.com" }])
-
# # SELECT * FROM users WHERE name = 'Joe' AND email = 'joe@example.com';
-
#
-
# This can make for more readable code in complex queries.
-
#
-
# Lastly, you can use sprintf-style % escapes in the template. This works slightly differently
-
# than the previous methods; you are responsible for ensuring that the values in the template
-
# are properly quoted. The values are passed to the connector for quoting, but the caller
-
# is responsible for ensuring they are enclosed in quotes in the resulting SQL. After quoting,
-
# the values are inserted using the same escapes as the Ruby core method +Kernel::sprintf+.
-
#
-
# User.where(["name = '%s' and email = '%s'", "Joe", "joe@example.com"])
-
# # SELECT * FROM users WHERE name = 'Joe' AND email = 'joe@example.com';
-
#
-
# If #where is called with multiple arguments, these are treated as if they were passed as
-
# the elements of a single array.
-
#
-
# User.where("name = :name and email = :email", { name: "Joe", email: "joe@example.com" })
-
# # SELECT * FROM users WHERE name = 'Joe' AND email = 'joe@example.com';
-
#
-
# When using strings to specify conditions, you can use any operator available from
-
# the database. While this provides the most flexibility, you can also unintentionally introduce
-
# dependencies on the underlying database. If your code is intended for general consumption,
-
# test with multiple database backends.
-
#
-
# === hash
-
#
-
# #where will also accept a hash condition, in which the keys are fields and the values
-
# are values to be searched for.
-
#
-
# Fields can be symbols or strings. Values can be single values, arrays, or ranges.
-
#
-
# User.where({ name: "Joe", email: "joe@example.com" })
-
# # SELECT * FROM users WHERE name = 'Joe' AND email = 'joe@example.com'
-
#
-
# User.where({ name: ["Alice", "Bob"]})
-
# # SELECT * FROM users WHERE name IN ('Alice', 'Bob')
-
#
-
# User.where({ created_at: (Time.now.midnight - 1.day)..Time.now.midnight })
-
# # SELECT * FROM users WHERE (created_at BETWEEN '2012-06-09 07:00:00.000000' AND '2012-06-10 07:00:00.000000')
-
#
-
# In the case of a belongs_to relationship, an association key can be used
-
# to specify the model if an ActiveRecord object is used as the value.
-
#
-
# author = Author.find(1)
-
#
-
# # The following queries will be equivalent:
-
# Post.where(author: author)
-
# Post.where(author_id: author)
-
#
-
# This also works with polymorphic belongs_to relationships:
-
#
-
# treasure = Treasure.create(name: 'gold coins')
-
# treasure.price_estimates << PriceEstimate.create(price: 125)
-
#
-
# # The following queries will be equivalent:
-
# PriceEstimate.where(estimate_of: treasure)
-
# PriceEstimate.where(estimate_of_type: 'Treasure', estimate_of_id: treasure)
-
#
-
# === Joins
-
#
-
# If the relation is the result of a join, you may create a condition which uses any of the
-
# tables in the join. For string and array conditions, use the table name in the condition.
-
#
-
# User.joins(:posts).where("posts.created_at < ?", Time.now)
-
#
-
# For hash conditions, you can either use the table name in the key, or use a sub-hash.
-
#
-
# User.joins(:posts).where({ "posts.published" => true })
-
# User.joins(:posts).where({ posts: { published: true } })
-
#
-
# === no argument
-
#
-
# If no argument is passed, #where returns a new instance of WhereChain, that
-
# can be chained with #not to return a new relation that negates the where clause.
-
#
-
# User.where.not(name: "Jon")
-
# # SELECT * FROM users WHERE name != 'Jon'
-
#
-
# See WhereChain for more details on #not.
-
#
-
# === blank condition
-
#
-
# If the condition is any blank-ish object, then #where is a no-op and returns
-
# the current relation.
-
1
def where(opts = :chain, *rest)
-
if :chain == opts
-
WhereChain.new(spawn)
-
elsif opts.blank?
-
self
-
else
-
spawn.where!(opts, *rest)
-
end
-
end
-
-
1
def where!(opts, *rest) # :nodoc:
-
opts = sanitize_forbidden_attributes(opts)
-
references!(PredicateBuilder.references(opts)) if Hash === opts
-
self.where_clause += where_clause_factory.build(opts, rest)
-
self
-
end
-
-
# Allows you to change a previously set where condition for a given attribute, instead of appending to that condition.
-
#
-
# Post.where(trashed: true).where(trashed: false)
-
# # WHERE `trashed` = 1 AND `trashed` = 0
-
#
-
# Post.where(trashed: true).rewhere(trashed: false)
-
# # WHERE `trashed` = 0
-
#
-
# Post.where(active: true).where(trashed: true).rewhere(trashed: false)
-
# # WHERE `active` = 1 AND `trashed` = 0
-
#
-
# This is short-hand for <tt>unscope(where: conditions.keys).where(conditions)</tt>.
-
# Note that unlike reorder, we're only unscoping the named conditions -- not the entire where statement.
-
1
def rewhere(conditions)
-
unscope(where: conditions.keys).where(conditions)
-
end
-
-
# Returns a new relation, which is the logical union of this relation and the one passed as an
-
# argument.
-
#
-
# The two relations must be structurally compatible: they must be scoping the same model, and
-
# they must differ only by #where (if no #group has been defined) or #having (if a #group is
-
# present). Neither relation may have a #limit, #offset, or #distinct set.
-
#
-
# Post.where("id = 1").or(Post.where("author_id = 3"))
-
# # SELECT `posts`.* FROM `posts` WHERE ((id = 1) OR (author_id = 3))
-
#
-
1
def or(other)
-
unless other.is_a? Relation
-
raise ArgumentError, "You have passed #{other.class.name} object to #or. Pass an ActiveRecord::Relation object instead."
-
end
-
-
spawn.or!(other)
-
end
-
-
1
def or!(other) # :nodoc:
-
incompatible_values = structurally_incompatible_values_for_or(other)
-
-
unless incompatible_values.empty?
-
raise ArgumentError, "Relation passed to #or must be structurally compatible. Incompatible values: #{incompatible_values}"
-
end
-
-
self.where_clause = self.where_clause.or(other.where_clause)
-
self.having_clause = having_clause.or(other.having_clause)
-
self.references_values += other.references_values
-
-
self
-
end
-
-
# Allows to specify a HAVING clause. Note that you can't use HAVING
-
# without also specifying a GROUP clause.
-
#
-
# Order.having('SUM(price) > 30').group('user_id')
-
1
def having(opts, *rest)
-
opts.blank? ? self : spawn.having!(opts, *rest)
-
end
-
-
1
def having!(opts, *rest) # :nodoc:
-
opts = sanitize_forbidden_attributes(opts)
-
references!(PredicateBuilder.references(opts)) if Hash === opts
-
-
self.having_clause += having_clause_factory.build(opts, rest)
-
self
-
end
-
-
# Specifies a limit for the number of records to retrieve.
-
#
-
# User.limit(10) # generated SQL has 'LIMIT 10'
-
#
-
# User.limit(10).limit(20) # generated SQL has 'LIMIT 20'
-
1
def limit(value)
-
spawn.limit!(value)
-
end
-
-
1
def limit!(value) # :nodoc:
-
self.limit_value = value
-
self
-
end
-
-
# Specifies the number of rows to skip before returning rows.
-
#
-
# User.offset(10) # generated SQL has "OFFSET 10"
-
#
-
# Should be used with order.
-
#
-
# User.offset(10).order("name ASC")
-
1
def offset(value)
-
spawn.offset!(value)
-
end
-
-
1
def offset!(value) # :nodoc:
-
self.offset_value = value
-
self
-
end
-
-
# Specifies locking settings (default to +true+). For more information
-
# on locking, please see ActiveRecord::Locking.
-
1
def lock(locks = true)
-
spawn.lock!(locks)
-
end
-
-
1
def lock!(locks = true) # :nodoc:
-
case locks
-
when String, TrueClass, NilClass
-
self.lock_value = locks || true
-
else
-
self.lock_value = false
-
end
-
-
self
-
end
-
-
# Returns a chainable relation with zero records.
-
#
-
# The returned relation implements the Null Object pattern. It is an
-
# object with defined null behavior and always returns an empty array of
-
# records without querying the database.
-
#
-
# Any subsequent condition chained to the returned relation will continue
-
# generating an empty relation and will not fire any query to the database.
-
#
-
# Used in cases where a method or scope could return zero records but the
-
# result needs to be chainable.
-
#
-
# For example:
-
#
-
# @posts = current_user.visible_posts.where(name: params[:name])
-
# # the visible_posts method is expected to return a chainable Relation
-
#
-
# def visible_posts
-
# case role
-
# when 'Country Manager'
-
# Post.where(country: country)
-
# when 'Reviewer'
-
# Post.published
-
# when 'Bad User'
-
# Post.none # It can't be chained if [] is returned.
-
# end
-
# end
-
#
-
1
def none
-
spawn.none!
-
end
-
-
1
def none! # :nodoc:
-
where!("1=0").extending!(NullRelation)
-
end
-
-
# Sets readonly attributes for the returned relation. If value is
-
# true (default), attempting to update a record will result in an error.
-
#
-
# users = User.readonly
-
# users.first.save
-
# => ActiveRecord::ReadOnlyRecord: User is marked as readonly
-
1
def readonly(value = true)
-
spawn.readonly!(value)
-
end
-
-
1
def readonly!(value = true) # :nodoc:
-
self.readonly_value = value
-
self
-
end
-
-
# Sets attributes to be used when creating new records from a
-
# relation object.
-
#
-
# users = User.where(name: 'Oscar')
-
# users.new.name # => 'Oscar'
-
#
-
# users = users.create_with(name: 'DHH')
-
# users.new.name # => 'DHH'
-
#
-
# You can pass +nil+ to #create_with to reset attributes:
-
#
-
# users = users.create_with(nil)
-
# users.new.name # => 'Oscar'
-
1
def create_with(value)
-
spawn.create_with!(value)
-
end
-
-
1
def create_with!(value) # :nodoc:
-
if value
-
value = sanitize_forbidden_attributes(value)
-
self.create_with_value = create_with_value.merge(value)
-
else
-
self.create_with_value = FROZEN_EMPTY_HASH
-
end
-
-
self
-
end
-
-
# Specifies table from which the records will be fetched. For example:
-
#
-
# Topic.select('title').from('posts')
-
# # SELECT title FROM posts
-
#
-
# Can accept other relation objects. For example:
-
#
-
# Topic.select('title').from(Topic.approved)
-
# # SELECT title FROM (SELECT * FROM topics WHERE approved = 't') subquery
-
#
-
# Topic.select('a.title').from(Topic.approved, :a)
-
# # SELECT a.title FROM (SELECT * FROM topics WHERE approved = 't') a
-
#
-
1
def from(value, subquery_name = nil)
-
spawn.from!(value, subquery_name)
-
end
-
-
1
def from!(value, subquery_name = nil) # :nodoc:
-
self.from_clause = Relation::FromClause.new(value, subquery_name)
-
self
-
end
-
-
# Specifies whether the records should be unique or not. For example:
-
#
-
# User.select(:name)
-
# # Might return two records with the same name
-
#
-
# User.select(:name).distinct
-
# # Returns 1 record per distinct name
-
#
-
# User.select(:name).distinct.distinct(false)
-
# # You can also remove the uniqueness
-
1
def distinct(value = true)
-
spawn.distinct!(value)
-
end
-
-
# Like #distinct, but modifies relation in place.
-
1
def distinct!(value = true) # :nodoc:
-
self.distinct_value = value
-
self
-
end
-
-
# Used to extend a scope with additional methods, either through
-
# a module or through a block provided.
-
#
-
# The object returned is a relation, which can be further extended.
-
#
-
# === Using a module
-
#
-
# module Pagination
-
# def page(number)
-
# # pagination code goes here
-
# end
-
# end
-
#
-
# scope = Model.all.extending(Pagination)
-
# scope.page(params[:page])
-
#
-
# You can also pass a list of modules:
-
#
-
# scope = Model.all.extending(Pagination, SomethingElse)
-
#
-
# === Using a block
-
#
-
# scope = Model.all.extending do
-
# def page(number)
-
# # pagination code goes here
-
# end
-
# end
-
# scope.page(params[:page])
-
#
-
# You can also use a block and a module list:
-
#
-
# scope = Model.all.extending(Pagination) do
-
# def per_page(number)
-
# # pagination code goes here
-
# end
-
# end
-
1
def extending(*modules, &block)
-
if modules.any? || block
-
spawn.extending!(*modules, &block)
-
else
-
self
-
end
-
end
-
-
1
def extending!(*modules, &block) # :nodoc:
-
modules << Module.new(&block) if block
-
modules.flatten!
-
-
self.extending_values += modules
-
extend(*extending_values) if extending_values.any?
-
-
self
-
end
-
-
# Reverse the existing order clause on the relation.
-
#
-
# User.order('name ASC').reverse_order # generated SQL has 'ORDER BY name DESC'
-
1
def reverse_order
-
spawn.reverse_order!
-
end
-
-
1
def reverse_order! # :nodoc:
-
orders = order_values.uniq
-
orders.reject!(&:blank?)
-
self.order_values = reverse_sql_order(orders)
-
self
-
end
-
-
1
def skip_query_cache!(value = true) # :nodoc:
-
self.skip_query_cache_value = value
-
self
-
end
-
-
# Returns the Arel object associated with the relation.
-
1
def arel(aliases = nil) # :nodoc:
-
2
@arel ||= build_arel(aliases)
-
end
-
-
# Returns a relation value with a given name
-
1
def get_value(name) # :nodoc:
-
44
@values.fetch(name, DEFAULT_VALUES[name])
-
end
-
-
1
protected
-
-
# Sets the relation value with the given name
-
1
def set_value(name, value) # :nodoc:
-
4
assert_mutability!
-
4
@values[name] = value
-
end
-
-
1
private
-
-
1
def assert_mutability!
-
4
raise ImmutableRelation if @loaded
-
4
raise ImmutableRelation if defined?(@arel) && @arel
-
end
-
-
1
def build_arel(aliases)
-
2
arel = Arel::SelectManager.new(table)
-
-
2
aliases = build_joins(arel, joins_values.flatten, aliases) unless joins_values.empty?
-
2
build_left_outer_joins(arel, left_outer_joins_values.flatten, aliases) unless left_outer_joins_values.empty?
-
-
2
arel.where(where_clause.ast) unless where_clause.empty?
-
2
arel.having(having_clause.ast) unless having_clause.empty?
-
2
if limit_value
-
limit_attribute = ActiveModel::Attribute.with_cast_value(
-
"LIMIT".freeze,
-
connection.sanitize_limit(limit_value),
-
Type.default_value,
-
)
-
arel.take(Arel::Nodes::BindParam.new(limit_attribute))
-
end
-
2
if offset_value
-
offset_attribute = ActiveModel::Attribute.with_cast_value(
-
"OFFSET".freeze,
-
offset_value.to_i,
-
Type.default_value,
-
)
-
arel.skip(Arel::Nodes::BindParam.new(offset_attribute))
-
end
-
2
arel.group(*arel_columns(group_values.uniq.reject(&:blank?))) unless group_values.empty?
-
-
2
build_order(arel)
-
-
2
build_select(arel)
-
-
2
arel.distinct(distinct_value)
-
2
arel.from(build_from) unless from_clause.empty?
-
2
arel.lock(lock_value) if lock_value
-
-
2
arel
-
end
-
-
1
def build_from
-
opts = from_clause.value
-
name = from_clause.name
-
case opts
-
when Relation
-
if opts.eager_loading?
-
opts = opts.send(:apply_join_dependency)
-
end
-
name ||= "subquery"
-
opts.arel.as(name.to_s)
-
else
-
opts
-
end
-
end
-
-
1
def build_left_outer_joins(manager, outer_joins, aliases)
-
buckets = outer_joins.group_by do |join|
-
case join
-
when Hash, Symbol, Array
-
:association_join
-
when ActiveRecord::Associations::JoinDependency
-
:stashed_join
-
else
-
raise ArgumentError, "only Hash, Symbol and Array are allowed"
-
end
-
end
-
-
build_join_query(manager, buckets, Arel::Nodes::OuterJoin, aliases)
-
end
-
-
1
def build_joins(manager, joins, aliases)
-
buckets = joins.group_by do |join|
-
case join
-
when String
-
:string_join
-
when Hash, Symbol, Array
-
:association_join
-
when ActiveRecord::Associations::JoinDependency
-
:stashed_join
-
when Arel::Nodes::Join
-
:join_node
-
else
-
raise "unknown class: %s" % join.class.name
-
end
-
end
-
-
build_join_query(manager, buckets, Arel::Nodes::InnerJoin, aliases)
-
end
-
-
1
def build_join_query(manager, buckets, join_type, aliases)
-
buckets.default = []
-
-
association_joins = buckets[:association_join]
-
stashed_joins = buckets[:stashed_join]
-
join_nodes = buckets[:join_node].uniq
-
string_joins = buckets[:string_join].map(&:strip).uniq
-
-
join_list = join_nodes + convert_join_strings_to_ast(string_joins)
-
alias_tracker = alias_tracker(join_list, aliases)
-
-
join_dependency = ActiveRecord::Associations::JoinDependency.new(
-
klass, table, association_joins
-
)
-
-
joins = join_dependency.join_constraints(stashed_joins, join_type, alias_tracker)
-
joins.each { |join| manager.from(join) }
-
-
manager.join_sources.concat(join_list)
-
-
alias_tracker.aliases
-
end
-
-
1
def convert_join_strings_to_ast(joins)
-
joins
-
.flatten
-
.reject(&:blank?)
-
.map { |join| table.create_string_join(Arel.sql(join)) }
-
end
-
-
1
def build_select(arel)
-
2
if select_values.any?
-
2
arel.project(*arel_columns(select_values.uniq))
-
elsif klass.ignored_columns.any?
-
arel.project(*klass.column_names.map { |field| arel_attribute(field) })
-
else
-
arel.project(table[Arel.star])
-
end
-
end
-
-
1
def arel_columns(columns)
-
2
columns.flat_map do |field|
-
2
case field
-
when Symbol
-
2
field = field.to_s
-
2
arel_column(field) { connection.quote_table_name(field) }
-
when String
-
arel_column(field) { field }
-
when Proc
-
field.call
-
else
-
field
-
end
-
end
-
end
-
-
1
def arel_column(field)
-
4
field = klass.attribute_alias(field) if klass.attribute_alias?(field)
-
4
from = from_clause.name || from_clause.value
-
-
4
if klass.columns_hash.key?(field) && (!from || table_name_matches?(from))
-
4
arel_attribute(field)
-
else
-
yield
-
end
-
end
-
-
1
def table_name_matches?(from)
-
/(?:\A|(?<!FROM)\s)(?:\b#{table.name}\b|#{connection.quote_table_name(table.name)})(?!\.)/i.match?(from.to_s)
-
end
-
-
1
def reverse_sql_order(order_query)
-
if order_query.empty?
-
return [arel_attribute(primary_key).desc] if primary_key
-
raise IrreversibleOrderError,
-
"Relation has no current order and table has no primary key to be used as default order"
-
end
-
-
order_query.flat_map do |o|
-
case o
-
when Arel::Attribute
-
o.desc
-
when Arel::Nodes::Ordering
-
o.reverse
-
when String
-
if does_not_support_reverse?(o)
-
raise IrreversibleOrderError, "Order #{o.inspect} can not be reversed automatically"
-
end
-
o.split(",").map! do |s|
-
s.strip!
-
s.gsub!(/\sasc\Z/i, " DESC") || s.gsub!(/\sdesc\Z/i, " ASC") || (s << " DESC")
-
end
-
else
-
o
-
end
-
end
-
end
-
-
1
def does_not_support_reverse?(order)
-
# Account for String subclasses like Arel::Nodes::SqlLiteral that
-
# override methods like #count.
-
order = String.new(order) unless order.instance_of?(String)
-
-
# Uses SQL function with multiple arguments.
-
(order.include?(",") && order.split(",").find { |section| section.count("(") != section.count(")") }) ||
-
# Uses "nulls first" like construction.
-
/nulls (first|last)\Z/i.match?(order)
-
end
-
-
1
def build_order(arel)
-
2
orders = order_values.uniq
-
2
orders.reject!(&:blank?)
-
-
2
arel.order(*orders) unless orders.empty?
-
end
-
-
1
VALID_DIRECTIONS = [:asc, :desc, :ASC, :DESC,
-
"asc", "desc", "ASC", "DESC"].to_set # :nodoc:
-
-
1
def validate_order_args(args)
-
2
args.each do |arg|
-
2
next unless arg.is_a?(Hash)
-
arg.each do |_key, value|
-
unless VALID_DIRECTIONS.include?(value)
-
raise ArgumentError,
-
"Direction \"#{value}\" is invalid. Valid directions are: #{VALID_DIRECTIONS.to_a.inspect}"
-
end
-
end
-
end
-
end
-
-
1
def preprocess_order_args(order_args)
-
2
order_args.map! do |arg|
-
2
klass.sanitize_sql_for_order(arg)
-
end
-
2
order_args.flatten!
-
-
4
@klass.enforce_raw_sql_whitelist(
-
4
order_args.flat_map { |a| a.is_a?(Hash) ? a.keys : a },
-
whitelist: AttributeMethods::ClassMethods::COLUMN_NAME_ORDER_WHITELIST
-
)
-
-
2
validate_order_args(order_args)
-
-
2
references = order_args.grep(String)
-
2
references.map! { |arg| arg =~ /^\W?(\w+)\W?\./ && $1 }.compact!
-
2
references!(references) if references.any?
-
-
# if a symbol is given we prepend the quoted table name
-
2
order_args.map! do |arg|
-
2
case arg
-
when Symbol
-
2
arg = arg.to_s
-
2
arel_column(arg) {
-
Arel.sql(connection.quote_table_name(arg))
-
}.asc
-
when Hash
-
arg.map { |field, dir|
-
case field
-
when Arel::Nodes::SqlLiteral
-
field.send(dir.downcase)
-
else
-
field = field.to_s
-
arel_column(field) {
-
Arel.sql(connection.quote_table_name(field))
-
}.send(dir.downcase)
-
end
-
}
-
else
-
arg
-
end
-
end.flatten!
-
end
-
-
# Checks to make sure that the arguments are not blank. Note that if some
-
# blank-like object were initially passed into the query method, then this
-
# method will not raise an error.
-
#
-
# Example:
-
#
-
# Post.references() # raises an error
-
# Post.references([]) # does not raise an error
-
#
-
# This particular method should be called with a method_name and the args
-
# passed into that method as an input. For example:
-
#
-
# def references(*args)
-
# check_if_method_has_arguments!("references", args)
-
# ...
-
# end
-
1
def check_if_method_has_arguments!(method_name, args)
-
2
if args.blank?
-
raise ArgumentError, "The method .#{method_name}() must contain arguments."
-
end
-
end
-
-
1
STRUCTURAL_OR_METHODS = Relation::VALUE_METHODS - [:extending, :where, :having, :unscope, :references]
-
1
def structurally_incompatible_values_for_or(other)
-
STRUCTURAL_OR_METHODS.reject do |method|
-
get_value(method) == other.get_value(method)
-
end
-
end
-
-
1
def where_clause_factory
-
@where_clause_factory ||= Relation::WhereClauseFactory.new(klass, predicate_builder)
-
end
-
1
alias having_clause_factory where_clause_factory
-
-
1
DEFAULT_VALUES = {
-
create_with: FROZEN_EMPTY_HASH,
-
where: Relation::WhereClause.empty,
-
having: Relation::WhereClause.empty,
-
from: Relation::FromClause.empty
-
}
-
-
1
Relation::MULTI_VALUE_METHODS.each do |value|
-
11
DEFAULT_VALUES[value] ||= FROZEN_EMPTY_ARRAY
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_support/core_ext/hash/except"
-
1
require "active_support/core_ext/hash/slice"
-
1
require "active_record/relation/merger"
-
-
1
module ActiveRecord
-
1
module SpawnMethods
-
# This is overridden by Associations::CollectionProxy
-
1
def spawn #:nodoc:
-
4
@delegate_to_klass ? klass.all : clone
-
end
-
-
# Merges in the conditions from <tt>other</tt>, if <tt>other</tt> is an ActiveRecord::Relation.
-
# Returns an array representing the intersection of the resulting records with <tt>other</tt>, if <tt>other</tt> is an array.
-
#
-
# Post.where(published: true).joins(:comments).merge( Comment.where(spam: false) )
-
# # Performs a single join query with both where conditions.
-
#
-
# recent_posts = Post.order('created_at DESC').first(5)
-
# Post.where(published: true).merge(recent_posts)
-
# # Returns the intersection of all published posts with the 5 most recently created posts.
-
# # (This is just an example. You'd probably want to do this with a single query!)
-
#
-
# Procs will be evaluated by merge:
-
#
-
# Post.where(published: true).merge(-> { joins(:comments) })
-
# # => Post.where(published: true).joins(:comments)
-
#
-
# This is mainly intended for sharing common conditions between multiple associations.
-
1
def merge(other)
-
if other.is_a?(Array)
-
records & other
-
elsif other
-
spawn.merge!(other)
-
else
-
raise ArgumentError, "invalid argument: #{other.inspect}."
-
end
-
end
-
-
1
def merge!(other) # :nodoc:
-
if other.is_a?(Hash)
-
Relation::HashMerger.new(self, other).merge
-
elsif other.is_a?(Relation)
-
Relation::Merger.new(self, other).merge
-
elsif other.respond_to?(:to_proc)
-
instance_exec(&other)
-
else
-
raise ArgumentError, "#{other.inspect} is not an ActiveRecord::Relation"
-
end
-
end
-
-
# Removes from the query the condition(s) specified in +skips+.
-
#
-
# Post.order('id asc').except(:order) # discards the order condition
-
# Post.where('id > 10').order('id asc').except(:where) # discards the where condition but keeps the order
-
1
def except(*skips)
-
relation_with values.except(*skips)
-
end
-
-
# Removes any condition from the query other than the one(s) specified in +onlies+.
-
#
-
# Post.order('id asc').only(:where) # discards the order condition
-
# Post.order('id asc').only(:where, :order) # uses the specified order
-
1
def only(*onlies)
-
relation_with values.slice(*onlies)
-
end
-
-
1
private
-
-
1
def relation_with(values)
-
result = Relation.create(klass, values: values)
-
result.extend(*extending_values) if extending_values.any?
-
result
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActiveRecord
-
1
class Relation
-
1
class WhereClause # :nodoc:
-
1
delegate :any?, :empty?, to: :predicates
-
-
1
def initialize(predicates)
-
1
@predicates = predicates
-
end
-
-
1
def +(other)
-
WhereClause.new(
-
predicates + other.predicates,
-
)
-
end
-
-
1
def -(other)
-
WhereClause.new(
-
predicates - other.predicates,
-
)
-
end
-
-
1
def merge(other)
-
WhereClause.new(
-
predicates_unreferenced_by(other) + other.predicates,
-
)
-
end
-
-
1
def except(*columns)
-
WhereClause.new(except_predicates(columns))
-
end
-
-
1
def or(other)
-
left = self - other
-
common = self - left
-
right = other - common
-
-
if left.empty? || right.empty?
-
common
-
else
-
or_clause = WhereClause.new(
-
[left.ast.or(right.ast)],
-
)
-
common + or_clause
-
end
-
end
-
-
1
def to_h(table_name = nil)
-
equalities = equalities(predicates)
-
if table_name
-
equalities = equalities.select do |node|
-
node.left.relation.name == table_name
-
end
-
end
-
-
equalities.map { |node|
-
name = node.left.name.to_s
-
value = extract_node_value(node.right)
-
[name, value]
-
}.to_h
-
end
-
-
1
def ast
-
Arel::Nodes::And.new(predicates_with_wrapped_sql_literals)
-
end
-
-
1
def ==(other)
-
other.is_a?(WhereClause) &&
-
predicates == other.predicates
-
end
-
-
1
def invert
-
WhereClause.new(inverted_predicates)
-
end
-
-
1
def self.empty
-
2
@empty ||= new([])
-
end
-
-
1
protected
-
-
1
attr_reader :predicates
-
-
1
def referenced_columns
-
@referenced_columns ||= begin
-
equality_nodes = predicates.select { |n| equality_node?(n) }
-
Set.new(equality_nodes, &:left)
-
end
-
end
-
-
1
private
-
1
def equalities(predicates)
-
equalities = []
-
-
predicates.each do |node|
-
case node
-
when Arel::Nodes::Equality
-
equalities << node
-
when Arel::Nodes::And
-
equalities.concat equalities(node.children)
-
end
-
end
-
-
equalities
-
end
-
-
1
def predicates_unreferenced_by(other)
-
predicates.reject do |n|
-
equality_node?(n) && other.referenced_columns.include?(n.left)
-
end
-
end
-
-
1
def equality_node?(node)
-
node.respond_to?(:operator) && node.operator == :==
-
end
-
-
1
def inverted_predicates
-
predicates.map { |node| invert_predicate(node) }
-
end
-
-
1
def invert_predicate(node)
-
case node
-
when NilClass
-
raise ArgumentError, "Invalid argument for .where.not(), got nil."
-
when Arel::Nodes::In
-
Arel::Nodes::NotIn.new(node.left, node.right)
-
when Arel::Nodes::Equality
-
Arel::Nodes::NotEqual.new(node.left, node.right)
-
when String
-
Arel::Nodes::Not.new(Arel::Nodes::SqlLiteral.new(node))
-
else
-
Arel::Nodes::Not.new(node)
-
end
-
end
-
-
1
def except_predicates(columns)
-
predicates.reject do |node|
-
case node
-
when Arel::Nodes::Between, Arel::Nodes::In, Arel::Nodes::NotIn, Arel::Nodes::Equality, Arel::Nodes::NotEqual, Arel::Nodes::LessThan, Arel::Nodes::LessThanOrEqual, Arel::Nodes::GreaterThan, Arel::Nodes::GreaterThanOrEqual
-
subrelation = (node.left.kind_of?(Arel::Attributes::Attribute) ? node.left : node.right)
-
columns.include?(subrelation.name.to_s)
-
end
-
end
-
end
-
-
1
def predicates_with_wrapped_sql_literals
-
non_empty_predicates.map do |node|
-
case node
-
when Arel::Nodes::SqlLiteral, ::String
-
wrap_sql_literal(node)
-
else node
-
end
-
end
-
end
-
-
1
ARRAY_WITH_EMPTY_STRING = [""]
-
1
def non_empty_predicates
-
predicates - ARRAY_WITH_EMPTY_STRING
-
end
-
-
1
def wrap_sql_literal(node)
-
if ::String === node
-
node = Arel.sql(node)
-
end
-
Arel::Nodes::Grouping.new(node)
-
end
-
-
1
def extract_node_value(node)
-
case node
-
when Array
-
node.map { |v| extract_node_value(v) }
-
when Arel::Nodes::Casted, Arel::Nodes::Quoted
-
node.val
-
when Arel::Nodes::BindParam
-
value = node.value
-
if value.respond_to?(:value_before_type_cast)
-
value.value_before_type_cast
-
else
-
value
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActiveRecord
-
1
class Relation
-
1
class WhereClauseFactory # :nodoc:
-
1
def initialize(klass, predicate_builder)
-
@klass = klass
-
@predicate_builder = predicate_builder
-
end
-
-
1
def build(opts, other)
-
case opts
-
when String, Array
-
parts = [klass.sanitize_sql(other.empty? ? opts : ([opts] + other))]
-
when Hash
-
attributes = predicate_builder.resolve_column_aliases(opts)
-
attributes.stringify_keys!
-
-
parts = predicate_builder.build_from_hash(attributes)
-
when Arel::Nodes::Node
-
parts = [opts]
-
else
-
raise ArgumentError, "Unsupported argument type: #{opts} (#{opts.class})"
-
end
-
-
WhereClause.new(parts)
-
end
-
-
1
protected
-
-
1
attr_reader :klass, :predicate_builder
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActiveRecord
-
###
-
# This class encapsulates a result returned from calling
-
# {#exec_query}[rdoc-ref:ConnectionAdapters::DatabaseStatements#exec_query]
-
# on any database connection adapter. For example:
-
#
-
# result = ActiveRecord::Base.connection.exec_query('SELECT id, title, body FROM posts')
-
# result # => #<ActiveRecord::Result:0xdeadbeef>
-
#
-
# # Get the column names of the result:
-
# result.columns
-
# # => ["id", "title", "body"]
-
#
-
# # Get the record values of the result:
-
# result.rows
-
# # => [[1, "title_1", "body_1"],
-
# [2, "title_2", "body_2"],
-
# ...
-
# ]
-
#
-
# # Get an array of hashes representing the result (column => value):
-
# result.to_hash
-
# # => [{"id" => 1, "title" => "title_1", "body" => "body_1"},
-
# {"id" => 2, "title" => "title_2", "body" => "body_2"},
-
# ...
-
# ]
-
#
-
# # ActiveRecord::Result also includes Enumerable.
-
# result.each do |row|
-
# puts row['title'] + " " + row['body']
-
# end
-
1
class Result
-
1
include Enumerable
-
-
1
attr_reader :columns, :rows, :column_types
-
-
1
def initialize(columns, rows, column_types = {})
-
4
@columns = columns
-
4
@rows = rows
-
4
@hash_rows = nil
-
4
@column_types = column_types
-
end
-
-
# Returns the number of elements in the rows array.
-
1
def length
-
@rows.length
-
end
-
-
# Calls the given block once for each element in row collection, passing
-
# row as parameter.
-
#
-
# Returns an +Enumerator+ if no block is given.
-
1
def each
-
if block_given?
-
hash_rows.each { |row| yield row }
-
else
-
hash_rows.to_enum { @rows.size }
-
end
-
end
-
-
# Returns an array of hashes representing each row record.
-
1
def to_hash
-
hash_rows
-
end
-
-
1
alias :map! :map
-
1
alias :collect! :map
-
-
# Returns true if there are no records, otherwise false.
-
1
def empty?
-
rows.empty?
-
end
-
-
# Returns an array of hashes representing each row record.
-
1
def to_ary
-
hash_rows
-
end
-
-
1
def [](idx)
-
hash_rows[idx]
-
end
-
-
# Returns the first record from the rows collection.
-
# If the rows collection is empty, returns +nil+.
-
1
def first
-
1
return nil if @rows.empty?
-
1
Hash[@columns.zip(@rows.first)]
-
end
-
-
# Returns the last record from the rows collection.
-
# If the rows collection is empty, returns +nil+.
-
1
def last
-
return nil if @rows.empty?
-
Hash[@columns.zip(@rows.last)]
-
end
-
-
1
def cast_values(type_overrides = {}) # :nodoc:
-
4
types = columns.map { |name| column_type(name, type_overrides) }
-
2
result = rows.map do |values|
-
4
types.zip(values).map { |type, value| type.deserialize(value) }
-
end
-
-
2
columns.one? ? result.map!(&:first) : result
-
end
-
-
1
def initialize_copy(other)
-
@columns = columns.dup
-
@rows = rows.dup
-
@column_types = column_types.dup
-
@hash_rows = nil
-
end
-
-
1
private
-
-
1
def column_type(name, type_overrides = {})
-
2
type_overrides.fetch(name) do
-
column_types.fetch(name, Type.default_value)
-
end
-
end
-
-
1
def hash_rows
-
@hash_rows ||=
-
begin
-
# We freeze the strings to prevent them getting duped when
-
# used as keys in ActiveRecord::Base's @attributes hash
-
columns = @columns.map { |c| c.dup.freeze }
-
@rows.map { |row|
-
# In the past we used Hash[columns.zip(row)]
-
# though elegant, the verbose way is much more efficient
-
# both time and memory wise cause it avoids a big array allocation
-
# this method is called a lot and needs to be micro optimised
-
hash = {}
-
-
index = 0
-
length = columns.length
-
-
while index < length
-
hash[columns[index]] = row[index]
-
index += 1
-
end
-
-
hash
-
}
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_support/per_thread_registry"
-
-
1
module ActiveRecord
-
# This is a thread locals registry for Active Record. For example:
-
#
-
# ActiveRecord::RuntimeRegistry.connection_handler
-
#
-
# returns the connection handler local to the current thread.
-
#
-
# See the documentation of ActiveSupport::PerThreadRegistry
-
# for further details.
-
1
class RuntimeRegistry # :nodoc:
-
1
extend ActiveSupport::PerThreadRegistry
-
-
1
attr_accessor :connection_handler, :sql_runtime
-
-
1
[:connection_handler, :sql_runtime].each do |val|
-
2
class_eval %{ def self.#{val}; instance.#{val}; end }, __FILE__, __LINE__
-
2
class_eval %{ def self.#{val}=(x); instance.#{val}=x; end }, __FILE__, __LINE__
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_record/scoping/default"
-
1
require "active_record/scoping/named"
-
-
1
module ActiveRecord
-
# This class is used to create a table that keeps track of which migrations
-
# have been applied to a given database. When a migration is run, its schema
-
# number is inserted in to the `SchemaMigration.table_name` so it doesn't need
-
# to be executed the next time.
-
1
class SchemaMigration < ActiveRecord::Base # :nodoc:
-
1
class << self
-
1
def primary_key
-
"version"
-
end
-
-
1
def table_name
-
4
"#{table_name_prefix}#{ActiveRecord::Base.schema_migrations_table_name}#{table_name_suffix}"
-
end
-
-
1
def table_exists?
-
2
connection.table_exists?(table_name)
-
end
-
-
1
def create_table
-
unless table_exists?
-
version_options = connection.internal_string_options_for_primary_key
-
-
connection.create_table(table_name, id: false) do |t|
-
t.string :version, version_options
-
end
-
end
-
end
-
-
1
def drop_table
-
connection.drop_table table_name, if_exists: true
-
end
-
-
1
def normalize_migration_number(number)
-
"%.3d" % number.to_i
-
end
-
-
1
def normalized_versions
-
all_versions.map { |v| normalize_migration_number v }
-
end
-
-
1
def all_versions
-
2
order(:version).pluck(:version)
-
end
-
end
-
-
1
def version
-
super.to_i
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActiveRecord
-
1
class TableMetadata # :nodoc:
-
1
delegate :foreign_type, :foreign_key, :join_primary_key, :join_foreign_key, to: :association, prefix: true
-
-
1
def initialize(klass, arel_table, association = nil)
-
1
@klass = klass
-
1
@arel_table = arel_table
-
1
@association = association
-
end
-
-
1
def resolve_column_aliases(hash)
-
new_hash = hash.dup
-
hash.each do |key, _|
-
if (key.is_a?(Symbol)) && klass.attribute_alias?(key)
-
new_hash[klass.attribute_alias(key)] = new_hash.delete(key)
-
end
-
end
-
new_hash
-
end
-
-
1
def arel_attribute(column_name)
-
if klass
-
klass.arel_attribute(column_name, arel_table)
-
else
-
arel_table[column_name]
-
end
-
end
-
-
1
def type(column_name)
-
if klass
-
klass.type_for_attribute(column_name)
-
else
-
Type.default_value
-
end
-
end
-
-
1
def has_column?(column_name)
-
klass && klass.columns_hash.key?(column_name.to_s)
-
end
-
-
1
def associated_with?(association_name)
-
klass && klass._reflect_on_association(association_name)
-
end
-
-
1
def associated_table(table_name)
-
association = klass._reflect_on_association(table_name) || klass._reflect_on_association(table_name.to_s.singularize)
-
-
if !association && table_name == arel_table.name
-
return self
-
elsif association && !association.polymorphic?
-
association_klass = association.klass
-
arel_table = association_klass.arel_table.alias(table_name)
-
else
-
type_caster = TypeCaster::Connection.new(klass, table_name)
-
association_klass = nil
-
arel_table = Arel::Table.new(table_name, type_caster: type_caster)
-
end
-
-
TableMetadata.new(association_klass, arel_table, association)
-
end
-
-
1
def polymorphic_association?
-
association && association.polymorphic?
-
end
-
-
1
def aggregated_with?(aggregation_name)
-
klass && reflect_on_aggregation(aggregation_name)
-
end
-
-
1
def reflect_on_aggregation(aggregation_name)
-
klass.reflect_on_aggregation(aggregation_name)
-
end
-
-
# TODO Change this to private once we've dropped Ruby 2.2 support.
-
# Workaround for Ruby 2.2 "private attribute?" warning.
-
1
protected
-
-
1
attr_reader :klass, :arel_table, :association
-
end
-
end
-
# frozen_string_literal: true
-
-
1
Rails.application.routes.draw do
-
1
get "/rails/active_storage/blobs/:signed_id/*filename" => "active_storage/blobs#show", as: :rails_service_blob
-
-
1
direct :rails_blob do |blob, options|
-
route_for(:rails_service_blob, blob.signed_id, blob.filename, options)
-
end
-
-
1
resolve("ActiveStorage::Blob") { |blob, options| route_for(:rails_blob, blob, options) }
-
1
resolve("ActiveStorage::Attachment") { |attachment, options| route_for(:rails_blob, attachment.blob, options) }
-
-
-
1
get "/rails/active_storage/representations/:signed_blob_id/:variation_key/*filename" => "active_storage/representations#show", as: :rails_blob_representation
-
-
1
direct :rails_representation do |representation, options|
-
signed_blob_id = representation.blob.signed_id
-
variation_key = representation.variation.key
-
filename = representation.blob.filename
-
-
route_for(:rails_blob_representation, signed_blob_id, variation_key, filename, options)
-
end
-
-
1
resolve("ActiveStorage::Variant") { |variant, options| route_for(:rails_representation, variant, options) }
-
1
resolve("ActiveStorage::Preview") { |preview, options| route_for(:rails_representation, preview, options) }
-
-
-
1
get "/rails/active_storage/disk/:encoded_key/*filename" => "active_storage/disk#show", as: :rails_disk_service
-
1
put "/rails/active_storage/disk/:encoded_token" => "active_storage/disk#update", as: :update_rails_disk_service
-
1
post "/rails/active_storage/direct_uploads" => "active_storage/direct_uploads#create", as: :rails_direct_uploads
-
end
-
# frozen_string_literal: true
-
-
1
require "action_dispatch"
-
1
require "action_dispatch/http/upload"
-
1
require "active_support/core_ext/module/delegation"
-
-
1
module ActiveStorage
-
# Abstract base class for the concrete ActiveStorage::Attached::One and ActiveStorage::Attached::Many
-
# classes that both provide proxy access to the blob association for a record.
-
1
class Attached
-
1
attr_reader :name, :record, :dependent
-
-
1
def initialize(name, record, dependent:)
-
@name, @record, @dependent = name, record, dependent
-
end
-
-
1
private
-
1
def create_blob_from(attachable)
-
case attachable
-
when ActiveStorage::Blob
-
attachable
-
when ActionDispatch::Http::UploadedFile, Rack::Test::UploadedFile
-
ActiveStorage::Blob.create_after_upload! \
-
io: attachable.open,
-
filename: attachable.original_filename,
-
content_type: attachable.content_type
-
when Hash
-
ActiveStorage::Blob.create_after_upload!(attachable)
-
when String
-
ActiveStorage::Blob.find_signed(attachable)
-
else
-
nil
-
end
-
end
-
end
-
end
-
-
1
require "active_storage/attached/one"
-
1
require "active_storage/attached/many"
-
1
require "active_storage/attached/macros"
-
# frozen_string_literal: true
-
-
1
module ActiveStorage
-
# Provides the class-level DSL for declaring that an Active Record model has attached blobs.
-
1
module Attached::Macros
-
# Specifies the relation between a single attachment and the model.
-
#
-
# class User < ActiveRecord::Base
-
# has_one_attached :avatar
-
# end
-
#
-
# There is no column defined on the model side, Active Storage takes
-
# care of the mapping between your records and the attachment.
-
#
-
# To avoid N+1 queries, you can include the attached blobs in your query like so:
-
#
-
# User.with_attached_avatar
-
#
-
# Under the covers, this relationship is implemented as a +has_one+ association to a
-
# ActiveStorage::Attachment record and a +has_one-through+ association to a
-
# ActiveStorage::Blob record. These associations are available as +avatar_attachment+
-
# and +avatar_blob+. But you shouldn't need to work with these associations directly in
-
# most circumstances.
-
#
-
# The system has been designed to having you go through the ActiveStorage::Attached::One
-
# proxy that provides the dynamic proxy to the associations and factory methods, like +attach+.
-
#
-
# If the +:dependent+ option isn't set, the attachment will be purged
-
# (i.e. destroyed) whenever the record is destroyed.
-
1
def has_one_attached(name, dependent: :purge_later)
-
class_eval <<-CODE, __FILE__, __LINE__ + 1
-
def #{name}
-
@active_storage_attached_#{name} ||= ActiveStorage::Attached::One.new("#{name}", self, dependent: #{dependent == :purge_later ? ":purge_later" : "false"})
-
end
-
-
def #{name}=(attachable)
-
#{name}.attach(attachable)
-
end
-
CODE
-
-
has_one :"#{name}_attachment", -> { where(name: name) }, class_name: "ActiveStorage::Attachment", as: :record, inverse_of: :record, dependent: false
-
has_one :"#{name}_blob", through: :"#{name}_attachment", class_name: "ActiveStorage::Blob", source: :blob
-
-
scope :"with_attached_#{name}", -> { includes("#{name}_attachment": :blob) }
-
-
if dependent == :purge_later
-
after_destroy_commit { public_send(name).purge_later }
-
else
-
before_destroy { public_send(name).detach }
-
end
-
end
-
-
# Specifies the relation between multiple attachments and the model.
-
#
-
# class Gallery < ActiveRecord::Base
-
# has_many_attached :photos
-
# end
-
#
-
# There are no columns defined on the model side, Active Storage takes
-
# care of the mapping between your records and the attachments.
-
#
-
# To avoid N+1 queries, you can include the attached blobs in your query like so:
-
#
-
# Gallery.where(user: Current.user).with_attached_photos
-
#
-
# Under the covers, this relationship is implemented as a +has_many+ association to a
-
# ActiveStorage::Attachment record and a +has_many-through+ association to a
-
# ActiveStorage::Blob record. These associations are available as +photos_attachments+
-
# and +photos_blobs+. But you shouldn't need to work with these associations directly in
-
# most circumstances.
-
#
-
# The system has been designed to having you go through the ActiveStorage::Attached::Many
-
# proxy that provides the dynamic proxy to the associations and factory methods, like +#attach+.
-
#
-
# If the +:dependent+ option isn't set, all the attachments will be purged
-
# (i.e. destroyed) whenever the record is destroyed.
-
1
def has_many_attached(name, dependent: :purge_later)
-
class_eval <<-CODE, __FILE__, __LINE__ + 1
-
def #{name}
-
@active_storage_attached_#{name} ||= ActiveStorage::Attached::Many.new("#{name}", self, dependent: #{dependent == :purge_later ? ":purge_later" : "false"})
-
end
-
-
def #{name}=(attachables)
-
#{name}.attach(attachables)
-
end
-
CODE
-
-
has_many :"#{name}_attachments", -> { where(name: name) }, as: :record, class_name: "ActiveStorage::Attachment", inverse_of: :record, dependent: false do
-
def purge
-
each(&:purge)
-
reset
-
end
-
-
def purge_later
-
each(&:purge_later)
-
reset
-
end
-
end
-
has_many :"#{name}_blobs", through: :"#{name}_attachments", class_name: "ActiveStorage::Blob", source: :blob
-
-
scope :"with_attached_#{name}", -> { includes("#{name}_attachments": :blob) }
-
-
if dependent == :purge_later
-
after_destroy_commit { public_send(name).purge_later }
-
else
-
before_destroy { public_send(name).detach }
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActiveStorage
-
# Decorated proxy object representing of multiple attachments to a model.
-
1
class Attached::Many < Attached
-
1
delegate_missing_to :attachments
-
-
# Returns all the associated attachment records.
-
#
-
# All methods called on this proxy object that aren't listed here will automatically be delegated to +attachments+.
-
1
def attachments
-
record.public_send("#{name}_attachments")
-
end
-
-
# Associates one or several attachments with the current record, saving them to the database.
-
#
-
# document.images.attach(params[:images]) # Array of ActionDispatch::Http::UploadedFile objects
-
# document.images.attach(params[:signed_blob_id]) # Signed reference to blob from direct upload
-
# document.images.attach(io: File.open("/path/to/racecar.jpg"), filename: "racecar.jpg", content_type: "image/jpg")
-
# document.images.attach([ first_blob, second_blob ])
-
1
def attach(*attachables)
-
attachables.flatten.collect do |attachable|
-
if record.new_record?
-
attachments.build(record: record, blob: create_blob_from(attachable))
-
else
-
attachments.create!(record: record, blob: create_blob_from(attachable))
-
end
-
end
-
end
-
-
# Returns true if any attachments has been made.
-
#
-
# class Gallery < ActiveRecord::Base
-
# has_many_attached :photos
-
# end
-
#
-
# Gallery.new.photos.attached? # => false
-
1
def attached?
-
attachments.any?
-
end
-
-
# Deletes associated attachments without purging them, leaving their respective blobs in place.
-
1
def detach
-
attachments.destroy_all if attached?
-
end
-
-
##
-
# :method: purge
-
#
-
# Directly purges each associated attachment (i.e. destroys the blobs and
-
# attachments and deletes the files on the service).
-
-
-
##
-
# :method: purge_later
-
#
-
# Purges each associated attachment through the queuing system.
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActiveStorage
-
# Representation of a single attachment to a model.
-
1
class Attached::One < Attached
-
1
delegate_missing_to :attachment
-
-
# Returns the associated attachment record.
-
#
-
# You don't have to call this method to access the attachment's methods as
-
# they are all available at the model level.
-
1
def attachment
-
record.public_send("#{name}_attachment")
-
end
-
-
# Associates a given attachment with the current record, saving it to the database.
-
#
-
# person.avatar.attach(params[:avatar]) # ActionDispatch::Http::UploadedFile object
-
# person.avatar.attach(params[:signed_blob_id]) # Signed reference to blob from direct upload
-
# person.avatar.attach(io: File.open("/path/to/face.jpg"), filename: "face.jpg", content_type: "image/jpg")
-
# person.avatar.attach(avatar_blob) # ActiveStorage::Blob object
-
1
def attach(attachable)
-
blob_was = blob if attached?
-
blob = create_blob_from(attachable)
-
-
unless blob == blob_was
-
transaction do
-
detach
-
write_attachment build_attachment(blob: blob)
-
end
-
-
blob_was.purge_later if blob_was && dependent == :purge_later
-
end
-
end
-
-
# Returns +true+ if an attachment has been made.
-
#
-
# class User < ActiveRecord::Base
-
# has_one_attached :avatar
-
# end
-
#
-
# User.new.avatar.attached? # => false
-
1
def attached?
-
attachment.present?
-
end
-
-
# Deletes the attachment without purging it, leaving its blob in place.
-
1
def detach
-
if attached?
-
attachment.destroy
-
write_attachment nil
-
end
-
end
-
-
# Directly purges the attachment (i.e. destroys the blob and
-
# attachment and deletes the file on the service).
-
1
def purge
-
if attached?
-
attachment.purge
-
write_attachment nil
-
end
-
end
-
-
# Purges the attachment through the queuing system.
-
1
def purge_later
-
if attached?
-
attachment.purge_later
-
end
-
end
-
-
1
private
-
1
delegate :transaction, to: :record
-
-
1
def build_attachment(blob:)
-
ActiveStorage::Attachment.new(record: record, name: name, blob: blob)
-
end
-
-
1
def write_attachment(attachment)
-
record.public_send("#{name}_attachment=", attachment)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_support"
-
1
require "active_support/time"
-
1
require "active_support/core_ext"
-
# frozen_string_literal: true
-
-
1
module ActiveSupport
-
# Backtraces often include many lines that are not relevant for the context
-
# under review. This makes it hard to find the signal amongst the backtrace
-
# noise, and adds debugging time. With a BacktraceCleaner, filters and
-
# silencers are used to remove the noisy lines, so that only the most relevant
-
# lines remain.
-
#
-
# Filters are used to modify lines of data, while silencers are used to remove
-
# lines entirely. The typical filter use case is to remove lengthy path
-
# information from the start of each line, and view file paths relevant to the
-
# app directory instead of the file system root. The typical silencer use case
-
# is to exclude the output of a noisy library from the backtrace, so that you
-
# can focus on the rest.
-
#
-
# bc = ActiveSupport::BacktraceCleaner.new
-
# bc.add_filter { |line| line.gsub(Rails.root.to_s, '') } # strip the Rails.root prefix
-
# bc.add_silencer { |line| line =~ /puma|rubygems/ } # skip any lines from puma or rubygems
-
# bc.clean(exception.backtrace) # perform the cleanup
-
#
-
# To reconfigure an existing BacktraceCleaner (like the default one in Rails)
-
# and show as much data as possible, you can always call
-
# <tt>BacktraceCleaner#remove_silencers!</tt>, which will restore the
-
# backtrace to a pristine state. If you need to reconfigure an existing
-
# BacktraceCleaner so that it does not filter or modify the paths of any lines
-
# of the backtrace, you can call <tt>BacktraceCleaner#remove_filters!</tt>
-
# These two methods will give you a completely untouched backtrace.
-
#
-
# Inspired by the Quiet Backtrace gem by thoughtbot.
-
1
class BacktraceCleaner
-
1
def initialize
-
1
@filters, @silencers = [], []
-
end
-
-
# Returns the backtrace after all filters and silencers have been run
-
# against it. Filters run first, then silencers.
-
1
def clean(backtrace, kind = :silent)
-
filtered = filter_backtrace(backtrace)
-
-
case kind
-
when :silent
-
silence(filtered)
-
when :noise
-
noise(filtered)
-
else
-
filtered
-
end
-
end
-
1
alias :filter :clean
-
-
# Adds a filter from the block provided. Each line in the backtrace will be
-
# mapped against this filter.
-
#
-
# # Will turn "/my/rails/root/app/models/person.rb" into "/app/models/person.rb"
-
# backtrace_cleaner.add_filter { |line| line.gsub(Rails.root, '') }
-
1
def add_filter(&block)
-
4
@filters << block
-
end
-
-
# Adds a silencer from the block provided. If the silencer returns +true+
-
# for a given line, it will be excluded from the clean backtrace.
-
#
-
# # Will reject all lines that include the word "puma", like "/gems/puma/server.rb" or "/app/my_puma_server/rb"
-
# backtrace_cleaner.add_silencer { |line| line =~ /puma/ }
-
1
def add_silencer(&block)
-
1
@silencers << block
-
end
-
-
# Removes all silencers, but leaves in the filters. Useful if your
-
# context of debugging suddenly expands as you suspect a bug in one of
-
# the libraries you use.
-
1
def remove_silencers!
-
@silencers = []
-
end
-
-
# Removes all filters, but leaves in the silencers. Useful if you suddenly
-
# need to see entire filepaths in the backtrace that you had already
-
# filtered out.
-
1
def remove_filters!
-
@filters = []
-
end
-
-
1
private
-
1
def filter_backtrace(backtrace)
-
@filters.each do |f|
-
backtrace = backtrace.map { |line| f.call(line) }
-
end
-
-
backtrace
-
end
-
-
1
def silence(backtrace)
-
@silencers.each do |s|
-
backtrace = backtrace.reject { |line| s.call(line) }
-
end
-
-
backtrace
-
end
-
-
1
def noise(backtrace)
-
backtrace - silence(backtrace)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "monitor"
-
-
1
module ActiveSupport
-
1
module Cache
-
# A cache store implementation which stores everything into memory in the
-
# same process. If you're running multiple Ruby on Rails server processes
-
# (which is the case if you're using Phusion Passenger or puma clustered mode),
-
# then this means that Rails server process instances won't be able
-
# to share cache data with each other and this may not be the most
-
# appropriate cache in that scenario.
-
#
-
# This cache has a bounded size specified by the :size options to the
-
# initializer (default is 32Mb). When the cache exceeds the allotted size,
-
# a cleanup will occur which tries to prune the cache down to three quarters
-
# of the maximum size by removing the least recently used entries.
-
#
-
# MemoryStore is thread-safe.
-
1
class MemoryStore < Store
-
1
def initialize(options = nil)
-
1
options ||= {}
-
1
super(options)
-
1
@data = {}
-
1
@key_access = {}
-
1
@max_size = options[:size] || 32.megabytes
-
1
@max_prune_time = options[:max_prune_time] || 2
-
1
@cache_size = 0
-
1
@monitor = Monitor.new
-
1
@pruning = false
-
end
-
-
# Delete all data stored in a given cache store.
-
1
def clear(options = nil)
-
synchronize do
-
@data.clear
-
@key_access.clear
-
@cache_size = 0
-
end
-
end
-
-
# Preemptively iterates through all stored keys and removes the ones which have expired.
-
1
def cleanup(options = nil)
-
options = merged_options(options)
-
instrument(:cleanup, size: @data.size) do
-
keys = synchronize { @data.keys }
-
keys.each do |key|
-
entry = @data[key]
-
delete_entry(key, options) if entry && entry.expired?
-
end
-
end
-
end
-
-
# To ensure entries fit within the specified memory prune the cache by removing the least
-
# recently accessed entries.
-
1
def prune(target_size, max_time = nil)
-
return if pruning?
-
@pruning = true
-
begin
-
start_time = Time.now
-
cleanup
-
instrument(:prune, target_size, from: @cache_size) do
-
keys = synchronize { @key_access.keys.sort { |a, b| @key_access[a].to_f <=> @key_access[b].to_f } }
-
keys.each do |key|
-
delete_entry(key, options)
-
return if @cache_size <= target_size || (max_time && Time.now - start_time > max_time)
-
end
-
end
-
ensure
-
@pruning = false
-
end
-
end
-
-
# Returns true if the cache is currently being pruned.
-
1
def pruning?
-
@pruning
-
end
-
-
# Increment an integer value in the cache.
-
1
def increment(name, amount = 1, options = nil)
-
modify_value(name, amount, options)
-
end
-
-
# Decrement an integer value in the cache.
-
1
def decrement(name, amount = 1, options = nil)
-
modify_value(name, -amount, options)
-
end
-
-
# Deletes cache entries if the cache key matches a given pattern.
-
1
def delete_matched(matcher, options = nil)
-
options = merged_options(options)
-
instrument(:delete_matched, matcher.inspect) do
-
matcher = key_matcher(matcher, options)
-
keys = synchronize { @data.keys }
-
keys.each do |key|
-
delete_entry(key, options) if key.match(matcher)
-
end
-
end
-
end
-
-
1
def inspect # :nodoc:
-
"<##{self.class.name} entries=#{@data.size}, size=#{@cache_size}, options=#{@options.inspect}>"
-
end
-
-
# Synchronize calls to the cache. This should be called wherever the underlying cache implementation
-
# is not thread safe.
-
1
def synchronize(&block) # :nodoc:
-
@monitor.synchronize(&block)
-
end
-
-
1
private
-
-
1
PER_ENTRY_OVERHEAD = 240
-
-
1
def cached_size(key, entry)
-
key.to_s.bytesize + entry.size + PER_ENTRY_OVERHEAD
-
end
-
-
1
def read_entry(key, options)
-
entry = @data[key]
-
synchronize do
-
if entry
-
@key_access[key] = Time.now.to_f
-
else
-
@key_access.delete(key)
-
end
-
end
-
entry
-
end
-
-
1
def write_entry(key, entry, options)
-
entry.dup_value!
-
synchronize do
-
old_entry = @data[key]
-
return false if @data.key?(key) && options[:unless_exist]
-
if old_entry
-
@cache_size -= (old_entry.size - entry.size)
-
else
-
@cache_size += cached_size(key, entry)
-
end
-
@key_access[key] = Time.now.to_f
-
@data[key] = entry
-
prune(@max_size * 0.75, @max_prune_time) if @cache_size > @max_size
-
true
-
end
-
end
-
-
1
def delete_entry(key, options)
-
synchronize do
-
@key_access.delete(key)
-
entry = @data.delete(key)
-
@cache_size -= cached_size(key, entry) if entry
-
!!entry
-
end
-
end
-
-
1
def modify_value(name, amount, options)
-
synchronize do
-
options = merged_options(options)
-
if num = read(name, options)
-
num = num.to_i + amount
-
write(name, num, options)
-
num
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActiveSupport
-
1
module Cache
-
# A cache store implementation which doesn't actually store anything. Useful in
-
# development and test environments where you don't want caching turned on but
-
# need to go through the caching interface.
-
#
-
# This cache does implement the local cache strategy, so values will actually
-
# be cached inside blocks that utilize this strategy. See
-
# ActiveSupport::Cache::Strategy::LocalCache for more details.
-
1
class NullStore < Store
-
1
prepend Strategy::LocalCache
-
-
1
def clear(options = nil)
-
end
-
-
1
def cleanup(options = nil)
-
end
-
-
1
def increment(name, amount = 1, options = nil)
-
end
-
-
1
def decrement(name, amount = 1, options = nil)
-
end
-
-
1
def delete_matched(matcher, options = nil)
-
end
-
-
1
private
-
1
def read_entry(key, options)
-
end
-
-
1
def write_entry(key, entry, options)
-
true
-
end
-
-
1
def delete_entry(key, options)
-
false
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_support/core_ext/object/duplicable"
-
1
require "active_support/core_ext/string/inflections"
-
1
require "active_support/per_thread_registry"
-
-
1
module ActiveSupport
-
1
module Cache
-
1
module Strategy
-
# Caches that implement LocalCache will be backed by an in-memory cache for the
-
# duration of a block. Repeated calls to the cache for the same key will hit the
-
# in-memory cache for faster access.
-
1
module LocalCache
-
1
autoload :Middleware, "active_support/cache/strategy/local_cache_middleware"
-
-
# Class for storing and registering the local caches.
-
1
class LocalCacheRegistry # :nodoc:
-
1
extend ActiveSupport::PerThreadRegistry
-
-
1
def initialize
-
1
@registry = {}
-
end
-
-
1
def cache_for(local_cache_key)
-
@registry[local_cache_key]
-
end
-
-
1
def set_cache_for(local_cache_key, value)
-
4
@registry[local_cache_key] = value
-
end
-
-
5
def self.set_cache_for(l, v); instance.set_cache_for l, v; end
-
1
def self.cache_for(l); instance.cache_for l; end
-
end
-
-
# Simple memory backed cache. This cache is not thread safe and is intended only
-
# for serving as a temporary memory cache for a single thread.
-
1
class LocalStore < Store
-
1
def initialize
-
2
super
-
2
@data = {}
-
end
-
-
# Don't allow synchronizing since it isn't thread safe.
-
1
def synchronize # :nodoc:
-
yield
-
end
-
-
1
def clear(options = nil)
-
@data.clear
-
end
-
-
1
def read_entry(key, options)
-
@data[key]
-
end
-
-
1
def read_multi_entries(keys, options)
-
values = {}
-
-
keys.each do |name|
-
entry = read_entry(name, options)
-
values[name] = entry.value if entry
-
end
-
-
values
-
end
-
-
1
def write_entry(key, value, options)
-
@data[key] = value
-
true
-
end
-
-
1
def delete_entry(key, options)
-
!!@data.delete(key)
-
end
-
-
1
def fetch_entry(key, options = nil) # :nodoc:
-
@data.fetch(key) { @data[key] = yield }
-
end
-
end
-
-
# Use a local cache for the duration of block.
-
1
def with_local_cache
-
use_temporary_local_cache(LocalStore.new) { yield }
-
end
-
-
# Middleware class can be inserted as a Rack handler to be local cache for the
-
# duration of request.
-
1
def middleware
-
2
@middleware ||= Middleware.new(
-
"ActiveSupport::Cache::Strategy::LocalCache",
-
local_cache_key)
-
end
-
-
1
def clear(options = nil) # :nodoc:
-
return super unless cache = local_cache
-
cache.clear(options)
-
super
-
end
-
-
1
def cleanup(options = nil) # :nodoc:
-
return super unless cache = local_cache
-
cache.clear
-
super
-
end
-
-
1
def increment(name, amount = 1, options = nil) # :nodoc:
-
return super unless local_cache
-
value = bypass_local_cache { super }
-
write_cache_value(name, value, options)
-
value
-
end
-
-
1
def decrement(name, amount = 1, options = nil) # :nodoc:
-
return super unless local_cache
-
value = bypass_local_cache { super }
-
write_cache_value(name, value, options)
-
value
-
end
-
-
1
private
-
1
def read_entry(key, options)
-
if cache = local_cache
-
cache.fetch_entry(key) { super }
-
else
-
super
-
end
-
end
-
-
1
def read_multi_entries(keys, options)
-
return super unless local_cache
-
-
local_entries = local_cache.read_multi_entries(keys, options)
-
missed_keys = keys - local_entries.keys
-
-
if missed_keys.any?
-
local_entries.merge!(super(missed_keys, options))
-
else
-
local_entries
-
end
-
end
-
-
1
def write_entry(key, entry, options)
-
if options[:unless_exist]
-
local_cache.delete_entry(key, options) if local_cache
-
else
-
local_cache.write_entry(key, entry, options) if local_cache
-
end
-
-
super
-
end
-
-
1
def delete_entry(key, options)
-
local_cache.delete_entry(key, options) if local_cache
-
super
-
end
-
-
1
def write_cache_value(name, value, options)
-
name = normalize_key(name, options)
-
cache = local_cache
-
cache.mute do
-
if value
-
cache.write(name, value, options)
-
else
-
cache.delete(name, options)
-
end
-
end
-
end
-
-
1
def local_cache_key
-
1
@local_cache_key ||= "#{self.class.name.underscore}_local_cache_#{object_id}".gsub(/[\/-]/, "_").to_sym
-
end
-
-
1
def local_cache
-
LocalCacheRegistry.cache_for(local_cache_key)
-
end
-
-
1
def bypass_local_cache
-
use_temporary_local_cache(nil) { yield }
-
end
-
-
1
def use_temporary_local_cache(temporary_cache)
-
save_cache = LocalCacheRegistry.cache_for(local_cache_key)
-
begin
-
LocalCacheRegistry.set_cache_for(local_cache_key, temporary_cache)
-
yield
-
ensure
-
LocalCacheRegistry.set_cache_for(local_cache_key, save_cache)
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "rack/body_proxy"
-
1
require "rack/utils"
-
-
1
module ActiveSupport
-
1
module Cache
-
1
module Strategy
-
1
module LocalCache
-
#--
-
# This class wraps up local storage for middlewares. Only the middleware method should
-
# construct them.
-
1
class Middleware # :nodoc:
-
1
attr_reader :name, :local_cache_key
-
-
1
def initialize(name, local_cache_key)
-
1
@name = name
-
1
@local_cache_key = local_cache_key
-
1
@app = nil
-
end
-
-
1
def new(app)
-
1
@app = app
-
1
self
-
end
-
-
1
def call(env)
-
2
LocalCacheRegistry.set_cache_for(local_cache_key, LocalStore.new)
-
2
response = @app.call(env)
-
2
response[2] = ::Rack::BodyProxy.new(response[2]) do
-
2
LocalCacheRegistry.set_cache_for(local_cache_key, nil)
-
end
-
2
cleanup_on_body_close = true
-
2
response
-
rescue Rack::Utils::InvalidParameterError
-
[400, {}, []]
-
ensure
-
LocalCacheRegistry.set_cache_for(local_cache_key, nil) unless
-
2
cleanup_on_body_close
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
Dir.glob(File.expand_path("core_ext/*.rb", __dir__)).each do |path|
-
23
require path
-
end
-
# frozen_string_literal: true
-
-
1
require "active_support/core_ext/big_decimal/conversions"
-
# frozen_string_literal: true
-
-
1
require "securerandom"
-
-
1
module Digest
-
1
module UUID
-
1
DNS_NAMESPACE = "k\xA7\xB8\x10\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" #:nodoc:
-
1
URL_NAMESPACE = "k\xA7\xB8\x11\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" #:nodoc:
-
1
OID_NAMESPACE = "k\xA7\xB8\x12\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" #:nodoc:
-
1
X500_NAMESPACE = "k\xA7\xB8\x14\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" #:nodoc:
-
-
# Generates a v5 non-random UUID (Universally Unique IDentifier).
-
#
-
# Using Digest::MD5 generates version 3 UUIDs; Digest::SHA1 generates version 5 UUIDs.
-
# uuid_from_hash always generates the same UUID for a given name and namespace combination.
-
#
-
# See RFC 4122 for details of UUID at: https://www.ietf.org/rfc/rfc4122.txt
-
1
def self.uuid_from_hash(hash_class, uuid_namespace, name)
-
if hash_class == Digest::MD5
-
version = 3
-
elsif hash_class == Digest::SHA1
-
version = 5
-
else
-
raise ArgumentError, "Expected Digest::SHA1 or Digest::MD5, got #{hash_class.name}."
-
end
-
-
hash = hash_class.new
-
hash.update(uuid_namespace)
-
hash.update(name)
-
-
ary = hash.digest.unpack("NnnnnN")
-
ary[2] = (ary[2] & 0x0FFF) | (version << 12)
-
ary[3] = (ary[3] & 0x3FFF) | 0x8000
-
-
"%08x-%04x-%04x-%04x-%04x%08x" % ary
-
end
-
-
# Convenience method for uuid_from_hash using Digest::MD5.
-
1
def self.uuid_v3(uuid_namespace, name)
-
uuid_from_hash(Digest::MD5, uuid_namespace, name)
-
end
-
-
# Convenience method for uuid_from_hash using Digest::SHA1.
-
1
def self.uuid_v5(uuid_namespace, name)
-
uuid_from_hash(Digest::SHA1, uuid_namespace, name)
-
end
-
-
# Convenience method for SecureRandom.uuid.
-
1
def self.uuid_v4
-
SecureRandom.uuid
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_support/core_ext/file/atomic"
-
# frozen_string_literal: true
-
-
1
require "fileutils"
-
-
1
class File
-
# Write to a file atomically. Useful for situations where you don't
-
# want other processes or threads to see half-written files.
-
#
-
# File.atomic_write('important.file') do |file|
-
# file.write('hello')
-
# end
-
#
-
# This method needs to create a temporary file. By default it will create it
-
# in the same directory as the destination file. If you don't like this
-
# behavior you can provide a different directory but it must be on the
-
# same physical filesystem as the file you're trying to write.
-
#
-
# File.atomic_write('/data/something.important', '/data/tmp') do |file|
-
# file.write('hello')
-
# end
-
1
def self.atomic_write(file_name, temp_dir = dirname(file_name))
-
require "tempfile" unless defined?(Tempfile)
-
-
Tempfile.open(".#{basename(file_name)}", temp_dir) do |temp_file|
-
temp_file.binmode
-
return_val = yield temp_file
-
temp_file.close
-
-
old_stat = if exist?(file_name)
-
# Get original file permissions
-
stat(file_name)
-
else
-
# If not possible, probe which are the default permissions in the
-
# destination directory.
-
probe_stat_in(dirname(file_name))
-
end
-
-
if old_stat
-
# Set correct permissions on new file
-
begin
-
chown(old_stat.uid, old_stat.gid, temp_file.path)
-
# This operation will affect filesystem ACL's
-
chmod(old_stat.mode, temp_file.path)
-
rescue Errno::EPERM, Errno::EACCES
-
# Changing file ownership failed, moving on.
-
end
-
end
-
-
# Overwrite original file with temp file
-
rename(temp_file.path, file_name)
-
return_val
-
end
-
end
-
-
# Private utility method.
-
1
def self.probe_stat_in(dir) #:nodoc:
-
basename = [
-
".permissions_check",
-
Thread.current.object_id,
-
Process.pid,
-
rand(1000000)
-
].join(".")
-
-
file_name = join(dir, basename)
-
FileUtils.touch(file_name)
-
stat(file_name)
-
ensure
-
FileUtils.rm_f(file_name) if file_name
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_support/core_ext/hash/compact"
-
1
require "active_support/core_ext/hash/conversions"
-
1
require "active_support/core_ext/hash/deep_merge"
-
1
require "active_support/core_ext/hash/except"
-
1
require "active_support/core_ext/hash/indifferent_access"
-
1
require "active_support/core_ext/hash/keys"
-
1
require "active_support/core_ext/hash/reverse_merge"
-
1
require "active_support/core_ext/hash/slice"
-
1
require "active_support/core_ext/hash/transform_values"
-
# frozen_string_literal: true
-
-
1
require "active_support/core_ext/integer/multiple"
-
1
require "active_support/core_ext/integer/inflections"
-
1
require "active_support/core_ext/integer/time"
-
# frozen_string_literal: true
-
-
1
require "active_support/inflector"
-
-
1
class Integer
-
# Ordinalize turns a number into an ordinal string used to denote the
-
# position in an ordered sequence such as 1st, 2nd, 3rd, 4th.
-
#
-
# 1.ordinalize # => "1st"
-
# 2.ordinalize # => "2nd"
-
# 1002.ordinalize # => "1002nd"
-
# 1003.ordinalize # => "1003rd"
-
# -11.ordinalize # => "-11th"
-
# -1001.ordinalize # => "-1001st"
-
1
def ordinalize
-
ActiveSupport::Inflector.ordinalize(self)
-
end
-
-
# Ordinal returns the suffix used to denote the position
-
# in an ordered sequence such as 1st, 2nd, 3rd, 4th.
-
#
-
# 1.ordinal # => "st"
-
# 2.ordinal # => "nd"
-
# 1002.ordinal # => "nd"
-
# 1003.ordinal # => "rd"
-
# -11.ordinal # => "th"
-
# -1001.ordinal # => "st"
-
1
def ordinal
-
ActiveSupport::Inflector.ordinal(self)
-
end
-
end
-
# frozen_string_literal: true
-
-
1
class Integer
-
# Check whether the integer is evenly divisible by the argument.
-
#
-
# 0.multiple_of?(0) # => true
-
# 6.multiple_of?(5) # => false
-
# 10.multiple_of?(2) # => true
-
1
def multiple_of?(number)
-
number != 0 ? self % number == 0 : zero?
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_support/core_ext/kernel/agnostics"
-
1
require "active_support/core_ext/kernel/concern"
-
1
require "active_support/core_ext/kernel/reporting"
-
1
require "active_support/core_ext/kernel/singleton_class"
-
# frozen_string_literal: true
-
-
1
class Object
-
# Makes backticks behave (somewhat more) similarly on all platforms.
-
# On win32 `nonexistent_command` raises Errno::ENOENT; on Unix, the
-
# spawned shell prints a message to stderr and sets $?. We emulate
-
# Unix on the former but not the latter.
-
1
def `(command) #:nodoc:
-
super
-
rescue Errno::ENOENT => e
-
STDERR.puts "#$0: #{e}"
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_support/core_ext/module/concerning"
-
-
1
module Kernel
-
1
module_function
-
-
# A shortcut to define a toplevel concern, not within a module.
-
#
-
# See Module::Concerning for more.
-
1
def concern(topic, &module_definition)
-
Object.concern topic, &module_definition
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActiveSupport
-
1
module MarshalWithAutoloading # :nodoc:
-
1
def load(source, proc = nil)
-
super(source, proc)
-
rescue ArgumentError, NameError => exc
-
if exc.message.match(%r|undefined class/module (.+?)(?:::)?\z|)
-
# try loading the class/module
-
loaded = $1.constantize
-
-
raise unless $1 == loaded.name
-
-
# if it is an IO we need to go back to read the object
-
source.rewind if source.respond_to?(:rewind)
-
retry
-
else
-
raise exc
-
end
-
end
-
end
-
end
-
-
1
Marshal.singleton_class.prepend(ActiveSupport::MarshalWithAutoloading)
-
# frozen_string_literal: true
-
-
1
require "active_support/core_ext/numeric/bytes"
-
1
require "active_support/core_ext/numeric/time"
-
1
require "active_support/core_ext/numeric/inquiry"
-
1
require "active_support/core_ext/numeric/conversions"
-
# frozen_string_literal: true
-
-
1
require "active_support/core_ext/big_decimal/conversions"
-
1
require "active_support/number_helper"
-
1
require "active_support/core_ext/module/deprecation"
-
-
1
module ActiveSupport::NumericWithFormat
-
# Provides options for converting numbers into formatted strings.
-
# Options are provided for phone numbers, currency, percentage,
-
# precision, positional notation, file size and pretty printing.
-
#
-
# ==== Options
-
#
-
# For details on which formats use which options, see ActiveSupport::NumberHelper
-
#
-
# ==== Examples
-
#
-
# Phone Numbers:
-
# 5551234.to_s(:phone) # => "555-1234"
-
# 1235551234.to_s(:phone) # => "123-555-1234"
-
# 1235551234.to_s(:phone, area_code: true) # => "(123) 555-1234"
-
# 1235551234.to_s(:phone, delimiter: ' ') # => "123 555 1234"
-
# 1235551234.to_s(:phone, area_code: true, extension: 555) # => "(123) 555-1234 x 555"
-
# 1235551234.to_s(:phone, country_code: 1) # => "+1-123-555-1234"
-
# 1235551234.to_s(:phone, country_code: 1, extension: 1343, delimiter: '.')
-
# # => "+1.123.555.1234 x 1343"
-
#
-
# Currency:
-
# 1234567890.50.to_s(:currency) # => "$1,234,567,890.50"
-
# 1234567890.506.to_s(:currency) # => "$1,234,567,890.51"
-
# 1234567890.506.to_s(:currency, precision: 3) # => "$1,234,567,890.506"
-
# 1234567890.506.to_s(:currency, locale: :fr) # => "1 234 567 890,51 ���"
-
# -1234567890.50.to_s(:currency, negative_format: '(%u%n)')
-
# # => "($1,234,567,890.50)"
-
# 1234567890.50.to_s(:currency, unit: '£', separator: ',', delimiter: '')
-
# # => "£1234567890,50"
-
# 1234567890.50.to_s(:currency, unit: '£', separator: ',', delimiter: '', format: '%n %u')
-
# # => "1234567890,50 £"
-
#
-
# Percentage:
-
# 100.to_s(:percentage) # => "100.000%"
-
# 100.to_s(:percentage, precision: 0) # => "100%"
-
# 1000.to_s(:percentage, delimiter: '.', separator: ',') # => "1.000,000%"
-
# 302.24398923423.to_s(:percentage, precision: 5) # => "302.24399%"
-
# 1000.to_s(:percentage, locale: :fr) # => "1 000,000%"
-
# 100.to_s(:percentage, format: '%n %') # => "100.000 %"
-
#
-
# Delimited:
-
# 12345678.to_s(:delimited) # => "12,345,678"
-
# 12345678.05.to_s(:delimited) # => "12,345,678.05"
-
# 12345678.to_s(:delimited, delimiter: '.') # => "12.345.678"
-
# 12345678.to_s(:delimited, delimiter: ',') # => "12,345,678"
-
# 12345678.05.to_s(:delimited, separator: ' ') # => "12,345,678 05"
-
# 12345678.05.to_s(:delimited, locale: :fr) # => "12 345 678,05"
-
# 98765432.98.to_s(:delimited, delimiter: ' ', separator: ',')
-
# # => "98 765 432,98"
-
#
-
# Rounded:
-
# 111.2345.to_s(:rounded) # => "111.235"
-
# 111.2345.to_s(:rounded, precision: 2) # => "111.23"
-
# 13.to_s(:rounded, precision: 5) # => "13.00000"
-
# 389.32314.to_s(:rounded, precision: 0) # => "389"
-
# 111.2345.to_s(:rounded, significant: true) # => "111"
-
# 111.2345.to_s(:rounded, precision: 1, significant: true) # => "100"
-
# 13.to_s(:rounded, precision: 5, significant: true) # => "13.000"
-
# 111.234.to_s(:rounded, locale: :fr) # => "111,234"
-
# 13.to_s(:rounded, precision: 5, significant: true, strip_insignificant_zeros: true)
-
# # => "13"
-
# 389.32314.to_s(:rounded, precision: 4, significant: true) # => "389.3"
-
# 1111.2345.to_s(:rounded, precision: 2, separator: ',', delimiter: '.')
-
# # => "1.111,23"
-
#
-
# Human-friendly size in Bytes:
-
# 123.to_s(:human_size) # => "123 Bytes"
-
# 1234.to_s(:human_size) # => "1.21 KB"
-
# 12345.to_s(:human_size) # => "12.1 KB"
-
# 1234567.to_s(:human_size) # => "1.18 MB"
-
# 1234567890.to_s(:human_size) # => "1.15 GB"
-
# 1234567890123.to_s(:human_size) # => "1.12 TB"
-
# 1234567890123456.to_s(:human_size) # => "1.1 PB"
-
# 1234567890123456789.to_s(:human_size) # => "1.07 EB"
-
# 1234567.to_s(:human_size, precision: 2) # => "1.2 MB"
-
# 483989.to_s(:human_size, precision: 2) # => "470 KB"
-
# 1234567.to_s(:human_size, precision: 2, separator: ',') # => "1,2 MB"
-
# 1234567890123.to_s(:human_size, precision: 5) # => "1.1228 TB"
-
# 524288000.to_s(:human_size, precision: 5) # => "500 MB"
-
#
-
# Human-friendly format:
-
# 123.to_s(:human) # => "123"
-
# 1234.to_s(:human) # => "1.23 Thousand"
-
# 12345.to_s(:human) # => "12.3 Thousand"
-
# 1234567.to_s(:human) # => "1.23 Million"
-
# 1234567890.to_s(:human) # => "1.23 Billion"
-
# 1234567890123.to_s(:human) # => "1.23 Trillion"
-
# 1234567890123456.to_s(:human) # => "1.23 Quadrillion"
-
# 1234567890123456789.to_s(:human) # => "1230 Quadrillion"
-
# 489939.to_s(:human, precision: 2) # => "490 Thousand"
-
# 489939.to_s(:human, precision: 4) # => "489.9 Thousand"
-
# 1234567.to_s(:human, precision: 4,
-
# significant: false) # => "1.2346 Million"
-
# 1234567.to_s(:human, precision: 1,
-
# separator: ',',
-
# significant: false) # => "1,2 Million"
-
1
def to_s(format = nil, options = nil)
-
97
case format
-
when nil
-
97
super()
-
when Integer, String
-
super(format)
-
when :phone
-
ActiveSupport::NumberHelper.number_to_phone(self, options || {})
-
when :currency
-
ActiveSupport::NumberHelper.number_to_currency(self, options || {})
-
when :percentage
-
ActiveSupport::NumberHelper.number_to_percentage(self, options || {})
-
when :delimited
-
ActiveSupport::NumberHelper.number_to_delimited(self, options || {})
-
when :rounded
-
ActiveSupport::NumberHelper.number_to_rounded(self, options || {})
-
when :human
-
ActiveSupport::NumberHelper.number_to_human(self, options || {})
-
when :human_size
-
ActiveSupport::NumberHelper.number_to_human_size(self, options || {})
-
when Symbol
-
super()
-
else
-
super(format)
-
end
-
end
-
end
-
-
# Ruby 2.4+ unifies Fixnum & Bignum into Integer.
-
1
if 0.class == Integer
-
1
Integer.prepend ActiveSupport::NumericWithFormat
-
else
-
Fixnum.prepend ActiveSupport::NumericWithFormat
-
Bignum.prepend ActiveSupport::NumericWithFormat
-
end
-
1
Float.prepend ActiveSupport::NumericWithFormat
-
1
BigDecimal.prepend ActiveSupport::NumericWithFormat
-
# frozen_string_literal: true
-
-
1
unless 1.respond_to?(:positive?) # TODO: Remove this file when we drop support to ruby < 2.3
-
class Numeric
-
# Returns true if the number is positive.
-
#
-
# 1.positive? # => true
-
# 0.positive? # => false
-
# -1.positive? # => false
-
def positive?
-
self > 0
-
end
-
-
# Returns true if the number is negative.
-
#
-
# -1.negative? # => true
-
# 0.negative? # => false
-
# 1.negative? # => false
-
def negative?
-
self < 0
-
end
-
end
-
-
class Complex
-
undef :positive?
-
undef :negative?
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "securerandom"
-
-
1
module SecureRandom
-
1
BASE58_ALPHABET = ("0".."9").to_a + ("A".."Z").to_a + ("a".."z").to_a - ["0", "O", "I", "l"]
-
# SecureRandom.base58 generates a random base58 string.
-
#
-
# The argument _n_ specifies the length, of the random string to be generated.
-
#
-
# If _n_ is not specified or is +nil+, 16 is assumed. It may be larger in the future.
-
#
-
# The result may contain alphanumeric characters except 0, O, I and l
-
#
-
# p SecureRandom.base58 # => "4kUgL2pdQMSCQtjE"
-
# p SecureRandom.base58(24) # => "77TMHrHJFvFDwodq8w7Ev2m7"
-
#
-
1
def self.base58(n = 16)
-
SecureRandom.random_bytes(n).unpack("C*").map do |byte|
-
idx = byte % 64
-
idx = SecureRandom.random_number(58) if idx >= 58
-
BASE58_ALPHABET[idx]
-
end.join
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_support/core_ext/string/conversions"
-
1
require "active_support/core_ext/string/filters"
-
1
require "active_support/core_ext/string/multibyte"
-
1
require "active_support/core_ext/string/starts_ends_with"
-
1
require "active_support/core_ext/string/inflections"
-
1
require "active_support/core_ext/string/access"
-
1
require "active_support/core_ext/string/behavior"
-
1
require "active_support/core_ext/string/output_safety"
-
1
require "active_support/core_ext/string/exclude"
-
1
require "active_support/core_ext/string/strip"
-
1
require "active_support/core_ext/string/inquiry"
-
1
require "active_support/core_ext/string/indent"
-
1
require "active_support/core_ext/string/zones"
-
# frozen_string_literal: true
-
-
1
class String
-
# The inverse of <tt>String#include?</tt>. Returns true if the string
-
# does not include the other string.
-
#
-
# "hello".exclude? "lo" # => false
-
# "hello".exclude? "ol" # => true
-
# "hello".exclude? ?h # => false
-
1
def exclude?(string)
-
!include?(string)
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActiveSupport
-
# Abstract super class that provides a thread-isolated attributes singleton, which resets automatically
-
# before and after each request. This allows you to keep all the per-request attributes easily
-
# available to the whole system.
-
#
-
# The following full app-like example demonstrates how to use a Current class to
-
# facilitate easy access to the global, per-request attributes without passing them deeply
-
# around everywhere:
-
#
-
# # app/models/current.rb
-
# class Current < ActiveSupport::CurrentAttributes
-
# attribute :account, :user
-
# attribute :request_id, :user_agent, :ip_address
-
#
-
# resets { Time.zone = nil }
-
#
-
# def user=(user)
-
# super
-
# self.account = user.account
-
# Time.zone = user.time_zone
-
# end
-
# end
-
#
-
# # app/controllers/concerns/authentication.rb
-
# module Authentication
-
# extend ActiveSupport::Concern
-
#
-
# included do
-
# before_action :authenticate
-
# end
-
#
-
# private
-
# def authenticate
-
# if authenticated_user = User.find_by(id: cookies.encrypted[:user_id])
-
# Current.user = authenticated_user
-
# else
-
# redirect_to new_session_url
-
# end
-
# end
-
# end
-
#
-
# # app/controllers/concerns/set_current_request_details.rb
-
# module SetCurrentRequestDetails
-
# extend ActiveSupport::Concern
-
#
-
# included do
-
# before_action do
-
# Current.request_id = request.uuid
-
# Current.user_agent = request.user_agent
-
# Current.ip_address = request.ip
-
# end
-
# end
-
# end
-
#
-
# class ApplicationController < ActionController::Base
-
# include Authentication
-
# include SetCurrentRequestDetails
-
# end
-
#
-
# class MessagesController < ApplicationController
-
# def create
-
# Current.account.messages.create(message_params)
-
# end
-
# end
-
#
-
# class Message < ApplicationRecord
-
# belongs_to :creator, default: -> { Current.user }
-
# after_create { |message| Event.create(record: message) }
-
# end
-
#
-
# class Event < ApplicationRecord
-
# before_create do
-
# self.request_id = Current.request_id
-
# self.user_agent = Current.user_agent
-
# self.ip_address = Current.ip_address
-
# end
-
# end
-
#
-
# A word of caution: It's easy to overdo a global singleton like Current and tangle your model as a result.
-
# Current should only be used for a few, top-level globals, like account, user, and request details.
-
# The attributes stuck in Current should be used by more or less all actions on all requests. If you start
-
# sticking controller-specific attributes in there, you're going to create a mess.
-
1
class CurrentAttributes
-
1
include ActiveSupport::Callbacks
-
1
define_callbacks :reset
-
-
1
class << self
-
# Returns singleton instance for this class in this thread. If none exists, one is created.
-
1
def instance
-
current_instances[name] ||= new
-
end
-
-
# Declares one or more attributes that will be given both class and instance accessor methods.
-
1
def attribute(*names)
-
generated_attribute_methods.module_eval do
-
names.each do |name|
-
define_method(name) do
-
attributes[name.to_sym]
-
end
-
-
define_method("#{name}=") do |attribute|
-
attributes[name.to_sym] = attribute
-
end
-
end
-
end
-
-
names.each do |name|
-
define_singleton_method(name) do
-
instance.public_send(name)
-
end
-
-
define_singleton_method("#{name}=") do |attribute|
-
instance.public_send("#{name}=", attribute)
-
end
-
end
-
end
-
-
# Calls this block after #reset is called on the instance. Used for resetting external collaborators, like Time.zone.
-
1
def resets(&block)
-
set_callback :reset, :after, &block
-
end
-
-
1
delegate :set, :reset, to: :instance
-
-
1
def reset_all # :nodoc:
-
4
current_instances.each_value(&:reset)
-
end
-
-
1
def clear_all # :nodoc:
-
reset_all
-
current_instances.clear
-
end
-
-
1
private
-
1
def generated_attribute_methods
-
@generated_attribute_methods ||= Module.new.tap { |mod| include mod }
-
end
-
-
1
def current_instances
-
4
Thread.current[:current_attributes_instances] ||= {}
-
end
-
-
1
def method_missing(name, *args, &block)
-
# Caches the method definition as a singleton method of the receiver.
-
#
-
# By letting #delegate handle it, we avoid an enclosure that'll capture args.
-
singleton_class.delegate name, to: :instance
-
-
send(name, *args, &block)
-
end
-
end
-
-
1
attr_accessor :attributes
-
-
1
def initialize
-
@attributes = {}
-
end
-
-
# Expose one or more attributes within a block. Old values are returned after the block concludes.
-
# Example demonstrating the common use of needing to set Current attributes outside the request-cycle:
-
#
-
# class Chat::PublicationJob < ApplicationJob
-
# def perform(attributes, room_number, creator)
-
# Current.set(person: creator) do
-
# Chat::Publisher.publish(attributes: attributes, room_number: room_number)
-
# end
-
# end
-
# end
-
1
def set(set_attributes)
-
old_attributes = compute_attributes(set_attributes.keys)
-
assign_attributes(set_attributes)
-
yield
-
ensure
-
assign_attributes(old_attributes)
-
end
-
-
# Reset all attributes. Should be called before and after actions, when used as a per-request singleton.
-
1
def reset
-
run_callbacks :reset do
-
self.attributes = {}
-
end
-
end
-
-
1
private
-
1
def assign_attributes(new_attributes)
-
new_attributes.each { |key, value| public_send("#{key}=", value) }
-
end
-
-
1
def compute_attributes(keys)
-
keys.collect { |key| [ key, public_send(key) ] }.to_h
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActiveSupport
-
1
class Digest #:nodoc:
-
1
class <<self
-
1
def hash_digest_class
-
@hash_digest_class ||= ::Digest::MD5
-
end
-
-
1
def hash_digest_class=(klass)
-
1
raise ArgumentError, "#{klass} is expected to implement hexdigest class method" unless klass.respond_to?(:hexdigest)
-
1
@hash_digest_class = klass
-
end
-
-
1
def hexdigest(arg)
-
hash_digest_class.hexdigest(arg)[0...32]
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
gem "minitest" # make sure we get the gem, not stdlib
-
1
require "minitest"
-
1
require "active_support/testing/tagged_logging"
-
1
require "active_support/testing/setup_and_teardown"
-
1
require "active_support/testing/assertions"
-
1
require "active_support/testing/deprecation"
-
1
require "active_support/testing/declarative"
-
1
require "active_support/testing/isolation"
-
1
require "active_support/testing/constant_lookup"
-
1
require "active_support/testing/time_helpers"
-
1
require "active_support/testing/file_fixtures"
-
-
1
module ActiveSupport
-
1
class TestCase < ::Minitest::Test
-
1
Assertion = Minitest::Assertion
-
-
1
class << self
-
# Sets the order in which test cases are run.
-
#
-
# ActiveSupport::TestCase.test_order = :random # => :random
-
#
-
# Valid values are:
-
# * +:random+ (to run tests in random order)
-
# * +:parallel+ (to run tests in parallel)
-
# * +:sorted+ (to run tests alphabetically by method name)
-
# * +:alpha+ (equivalent to +:sorted+)
-
1
def test_order=(new_order)
-
ActiveSupport.test_order = new_order
-
end
-
-
# Returns the order in which test cases are run.
-
#
-
# ActiveSupport::TestCase.test_order # => :random
-
#
-
# Possible values are +:random+, +:parallel+, +:alpha+, +:sorted+.
-
# Defaults to +:random+.
-
1
def test_order
-
17
ActiveSupport.test_order ||= :random
-
end
-
end
-
-
1
alias_method :method_name, :name
-
-
1
include ActiveSupport::Testing::TaggedLogging
-
1
prepend ActiveSupport::Testing::SetupAndTeardown
-
1
include ActiveSupport::Testing::Assertions
-
1
include ActiveSupport::Testing::Deprecation
-
1
include ActiveSupport::Testing::TimeHelpers
-
1
include ActiveSupport::Testing::FileFixtures
-
1
extend ActiveSupport::Testing::Declarative
-
-
# test/unit backwards compatibility methods
-
1
alias :assert_raise :assert_raises
-
1
alias :assert_not_empty :refute_empty
-
1
alias :assert_not_equal :refute_equal
-
1
alias :assert_not_in_delta :refute_in_delta
-
1
alias :assert_not_in_epsilon :refute_in_epsilon
-
1
alias :assert_not_includes :refute_includes
-
1
alias :assert_not_instance_of :refute_instance_of
-
1
alias :assert_not_kind_of :refute_kind_of
-
1
alias :assert_no_match :refute_match
-
1
alias :assert_not_nil :refute_nil
-
1
alias :assert_not_operator :refute_operator
-
1
alias :assert_not_predicate :refute_predicate
-
1
alias :assert_not_respond_to :refute_respond_to
-
1
alias :assert_not_same :refute_same
-
-
1
ActiveSupport.run_load_hooks(:active_support_test_case, self)
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActiveSupport
-
1
module Testing
-
1
module Assertions
-
1
UNTRACKED = Object.new # :nodoc:
-
-
# Asserts that an expression is not truthy. Passes if <tt>object</tt> is
-
# +nil+ or +false+. "Truthy" means "considered true in a conditional"
-
# like <tt>if foo</tt>.
-
#
-
# assert_not nil # => true
-
# assert_not false # => true
-
# assert_not 'foo' # => Expected "foo" to be nil or false
-
#
-
# An error message can be specified.
-
#
-
# assert_not foo, 'foo should be false'
-
1
def assert_not(object, message = nil)
-
message ||= "Expected #{mu_pp(object)} to be nil or false"
-
assert !object, message
-
end
-
-
# Assertion that the block should not raise an exception.
-
#
-
# Passes if evaluated code in the yielded block raises no exception.
-
#
-
# assert_nothing_raised do
-
# perform_service(param: 'no_exception')
-
# end
-
1
def assert_nothing_raised
-
yield
-
end
-
-
# Test numeric difference between the return value of an expression as a
-
# result of what is evaluated in the yielded block.
-
#
-
# assert_difference 'Article.count' do
-
# post :create, params: { article: {...} }
-
# end
-
#
-
# An arbitrary expression is passed in and evaluated.
-
#
-
# assert_difference 'Article.last.comments(:reload).size' do
-
# post :create, params: { comment: {...} }
-
# end
-
#
-
# An arbitrary positive or negative difference can be specified.
-
# The default is <tt>1</tt>.
-
#
-
# assert_difference 'Article.count', -1 do
-
# post :delete, params: { id: ... }
-
# end
-
#
-
# An array of expressions can also be passed in and evaluated.
-
#
-
# assert_difference [ 'Article.count', 'Post.count' ], 2 do
-
# post :create, params: { article: {...} }
-
# end
-
#
-
# A hash of expressions/numeric differences can also be passed in and evaluated.
-
#
-
# assert_difference ->{ Article.count } => 1, ->{ Notification.count } => 2 do
-
# post :create, params: { article: {...} }
-
# end
-
#
-
# A lambda or a list of lambdas can be passed in and evaluated:
-
#
-
# assert_difference ->{ Article.count }, 2 do
-
# post :create, params: { article: {...} }
-
# end
-
#
-
# assert_difference [->{ Article.count }, ->{ Post.count }], 2 do
-
# post :create, params: { article: {...} }
-
# end
-
#
-
# An error message can be specified.
-
#
-
# assert_difference 'Article.count', -1, 'An Article should be destroyed' do
-
# post :delete, params: { id: ... }
-
# end
-
1
def assert_difference(expression, *args, &block)
-
expressions =
-
if expression.is_a?(Hash)
-
message = args[0]
-
expression
-
else
-
difference = args[0] || 1
-
message = args[1]
-
Hash[Array(expression).map { |e| [e, difference] }]
-
end
-
-
exps = expressions.keys.map { |e|
-
e.respond_to?(:call) ? e : lambda { eval(e, block.binding) }
-
}
-
before = exps.map(&:call)
-
-
retval = yield
-
-
expressions.zip(exps, before) do |(code, diff), exp, before_value|
-
error = "#{code.inspect} didn't change by #{diff}"
-
error = "#{message}.\n#{error}" if message
-
assert_equal(before_value + diff, exp.call, error)
-
end
-
-
retval
-
end
-
-
# Assertion that the numeric result of evaluating an expression is not
-
# changed before and after invoking the passed in block.
-
#
-
# assert_no_difference 'Article.count' do
-
# post :create, params: { article: invalid_attributes }
-
# end
-
#
-
# An error message can be specified.
-
#
-
# assert_no_difference 'Article.count', 'An Article should not be created' do
-
# post :create, params: { article: invalid_attributes }
-
# end
-
1
def assert_no_difference(expression, message = nil, &block)
-
assert_difference expression, 0, message, &block
-
end
-
-
# Assertion that the result of evaluating an expression is changed before
-
# and after invoking the passed in block.
-
#
-
# assert_changes 'Status.all_good?' do
-
# post :create, params: { status: { ok: false } }
-
# end
-
#
-
# You can pass the block as a string to be evaluated in the context of
-
# the block. A lambda can be passed for the block as well.
-
#
-
# assert_changes -> { Status.all_good? } do
-
# post :create, params: { status: { ok: false } }
-
# end
-
#
-
# The assertion is useful to test side effects. The passed block can be
-
# anything that can be converted to string with #to_s.
-
#
-
# assert_changes :@object do
-
# @object = 42
-
# end
-
#
-
# The keyword arguments :from and :to can be given to specify the
-
# expected initial value and the expected value after the block was
-
# executed.
-
#
-
# assert_changes :@object, from: nil, to: :foo do
-
# @object = :foo
-
# end
-
#
-
# An error message can be specified.
-
#
-
# assert_changes -> { Status.all_good? }, 'Expected the status to be bad' do
-
# post :create, params: { status: { incident: true } }
-
# end
-
1
def assert_changes(expression, message = nil, from: UNTRACKED, to: UNTRACKED, &block)
-
exp = expression.respond_to?(:call) ? expression : -> { eval(expression.to_s, block.binding) }
-
-
before = exp.call
-
retval = yield
-
-
unless from == UNTRACKED
-
error = "#{expression.inspect} isn't #{from.inspect}"
-
error = "#{message}.\n#{error}" if message
-
assert from === before, error
-
end
-
-
after = exp.call
-
-
error = "#{expression.inspect} didn't change"
-
error = "#{error}. It was already #{to}" if before == to
-
error = "#{message}.\n#{error}" if message
-
assert before != after, error
-
-
unless to == UNTRACKED
-
error = "#{expression.inspect} didn't change to #{to}"
-
error = "#{message}.\n#{error}" if message
-
assert to === after, error
-
end
-
-
retval
-
end
-
-
# Assertion that the result of evaluating an expression is not changed before
-
# and after invoking the passed in block.
-
#
-
# assert_no_changes 'Status.all_good?' do
-
# post :create, params: { status: { ok: true } }
-
# end
-
#
-
# An error message can be specified.
-
#
-
# assert_no_changes -> { Status.all_good? }, 'Expected the status to be good' do
-
# post :create, params: { status: { ok: false } }
-
# end
-
1
def assert_no_changes(expression, message = nil, &block)
-
exp = expression.respond_to?(:call) ? expression : -> { eval(expression.to_s, block.binding) }
-
-
before = exp.call
-
retval = yield
-
after = exp.call
-
-
error = "#{expression.inspect} did change to #{after}"
-
error = "#{message}.\n#{error}" if message
-
assert before == after, error
-
-
retval
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
gem "minitest"
-
-
1
require "minitest"
-
-
1
Minitest.autorun
-
# frozen_string_literal: true
-
-
1
require "active_support/concern"
-
1
require "active_support/inflector"
-
-
1
module ActiveSupport
-
1
module Testing
-
# Resolves a constant from a minitest spec name.
-
#
-
# Given the following spec-style test:
-
#
-
# describe WidgetsController, :index do
-
# describe "authenticated user" do
-
# describe "returns widgets" do
-
# it "has a controller that exists" do
-
# assert_kind_of WidgetsController, @controller
-
# end
-
# end
-
# end
-
# end
-
#
-
# The test will have the following name:
-
#
-
# "WidgetsController::index::authenticated user::returns widgets"
-
#
-
# The constant WidgetsController can be resolved from the name.
-
# The following code will resolve the constant:
-
#
-
# controller = determine_constant_from_test_name(name) do |constant|
-
# Class === constant && constant < ::ActionController::Metal
-
# end
-
1
module ConstantLookup
-
1
extend ::ActiveSupport::Concern
-
-
1
module ClassMethods # :nodoc:
-
1
def determine_constant_from_test_name(test_name)
-
names = test_name.split "::"
-
while names.size > 0 do
-
names.last.sub!(/Test$/, "")
-
begin
-
constant = names.join("::").safe_constantize
-
break(constant) if yield(constant)
-
ensure
-
names.pop
-
end
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActiveSupport
-
1
module Testing
-
1
module Declarative
-
1
unless defined?(Spec)
-
# Helper to define a test method using a String. Under the hood, it replaces
-
# spaces with underscores and defines the test method.
-
#
-
# test "verify something" do
-
# ...
-
# end
-
1
def test(name, &block)
-
6
test_name = "test_#{name.gsub(/\s+/, '_')}".to_sym
-
6
defined = method_defined? test_name
-
6
raise "#{test_name} is already defined in #{self}" if defined
-
6
if block_given?
-
6
define_method(test_name, &block)
-
else
-
define_method(test_name) do
-
flunk "No implementation provided for #{name}"
-
end
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_support/deprecation"
-
1
require "active_support/core_ext/regexp"
-
-
1
module ActiveSupport
-
1
module Testing
-
1
module Deprecation #:nodoc:
-
1
def assert_deprecated(match = nil, deprecator = nil, &block)
-
result, warnings = collect_deprecations(deprecator, &block)
-
assert !warnings.empty?, "Expected a deprecation warning within the block but received none"
-
if match
-
match = Regexp.new(Regexp.escape(match)) unless match.is_a?(Regexp)
-
assert warnings.any? { |w| match.match?(w) }, "No deprecation warning matched #{match}: #{warnings.join(', ')}"
-
end
-
result
-
end
-
-
1
def assert_not_deprecated(deprecator = nil, &block)
-
result, deprecations = collect_deprecations(deprecator, &block)
-
assert deprecations.empty?, "Expected no deprecation warning within the block but received #{deprecations.size}: \n #{deprecations * "\n "}"
-
result
-
end
-
-
1
def collect_deprecations(deprecator = nil)
-
deprecator ||= ActiveSupport::Deprecation
-
old_behavior = deprecator.behavior
-
deprecations = []
-
deprecator.behavior = Proc.new do |message, callstack|
-
deprecations << message
-
end
-
result = yield
-
[result, deprecations]
-
ensure
-
deprecator.behavior = old_behavior
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActiveSupport
-
1
module Testing
-
# Adds simple access to sample files called file fixtures.
-
# File fixtures are normal files stored in
-
# <tt>ActiveSupport::TestCase.file_fixture_path</tt>.
-
#
-
# File fixtures are represented as +Pathname+ objects.
-
# This makes it easy to extract specific information:
-
#
-
# file_fixture("example.txt").read # get the file's content
-
# file_fixture("example.mp3").size # get the file size
-
1
module FileFixtures
-
1
extend ActiveSupport::Concern
-
-
1
included do
-
1
class_attribute :file_fixture_path, instance_writer: false
-
end
-
-
# Returns a +Pathname+ to the fixture file named +fixture_name+.
-
#
-
# Raises +ArgumentError+ if +fixture_name+ can't be found.
-
1
def file_fixture(fixture_name)
-
path = Pathname.new(File.join(file_fixture_path, fixture_name))
-
-
if path.exist?
-
path
-
else
-
msg = "the directory '%s' does not contain a file named '%s'"
-
raise ArgumentError, msg % [file_fixture_path, fixture_name]
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActiveSupport
-
1
module Testing
-
1
module Isolation
-
1
require "thread"
-
-
1
def self.included(klass) #:nodoc:
-
klass.class_eval do
-
parallelize_me!
-
end
-
end
-
-
1
def self.forking_env?
-
1
!ENV["NO_FORK"] && Process.respond_to?(:fork)
-
end
-
-
1
def run
-
serialized = run_in_isolation do
-
super
-
end
-
-
Marshal.load(serialized)
-
end
-
-
1
module Forking
-
1
def run_in_isolation(&blk)
-
read, write = IO.pipe
-
read.binmode
-
write.binmode
-
-
pid = fork do
-
read.close
-
yield
-
begin
-
if error?
-
failures.map! { |e|
-
begin
-
Marshal.dump e
-
e
-
rescue TypeError
-
ex = Exception.new e.message
-
ex.set_backtrace e.backtrace
-
Minitest::UnexpectedError.new ex
-
end
-
}
-
end
-
test_result = defined?(Minitest::Result) ? Minitest::Result.from(self) : dup
-
result = Marshal.dump(test_result)
-
end
-
-
write.puts [result].pack("m")
-
exit!
-
end
-
-
write.close
-
result = read.read
-
Process.wait2(pid)
-
result.unpack("m")[0]
-
end
-
end
-
-
1
module Subprocess
-
1
ORIG_ARGV = ARGV.dup unless defined?(ORIG_ARGV)
-
-
# Crazy H4X to get this working in windows / jruby with
-
# no forking.
-
1
def run_in_isolation(&blk)
-
require "tempfile"
-
-
if ENV["ISOLATION_TEST"]
-
yield
-
test_result = defined?(Minitest::Result) ? Minitest::Result.from(self) : dup
-
File.open(ENV["ISOLATION_OUTPUT"], "w") do |file|
-
file.puts [Marshal.dump(test_result)].pack("m")
-
end
-
exit!
-
else
-
Tempfile.open("isolation") do |tmpfile|
-
env = {
-
"ISOLATION_TEST" => self.class.name,
-
"ISOLATION_OUTPUT" => tmpfile.path
-
}
-
-
test_opts = "-n#{self.class.name}##{name}"
-
-
load_path_args = []
-
$-I.each do |p|
-
load_path_args << "-I"
-
load_path_args << File.expand_path(p)
-
end
-
-
child = IO.popen([env, Gem.ruby, *load_path_args, $0, *ORIG_ARGV, test_opts])
-
-
begin
-
Process.wait(child.pid)
-
rescue Errno::ECHILD # The child process may exit before we wait
-
nil
-
end
-
-
return tmpfile.read.unpack("m")[0]
-
end
-
end
-
end
-
end
-
-
1
include forking_env? ? Forking : Subprocess
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_support/callbacks"
-
-
1
module ActiveSupport
-
1
module Testing
-
# Adds support for +setup+ and +teardown+ callbacks.
-
# These callbacks serve as a replacement to overwriting the
-
# <tt>#setup</tt> and <tt>#teardown</tt> methods of your TestCase.
-
#
-
# class ExampleTest < ActiveSupport::TestCase
-
# setup do
-
# # ...
-
# end
-
#
-
# teardown do
-
# # ...
-
# end
-
# end
-
1
module SetupAndTeardown
-
1
def self.prepended(klass)
-
1
klass.include ActiveSupport::Callbacks
-
1
klass.define_callbacks :setup, :teardown
-
1
klass.extend ClassMethods
-
end
-
-
1
module ClassMethods
-
# Add a callback, which runs before <tt>TestCase#setup</tt>.
-
1
def setup(*args, &block)
-
4
set_callback(:setup, :before, *args, &block)
-
end
-
-
# Add a callback, which runs after <tt>TestCase#teardown</tt>.
-
1
def teardown(*args, &block)
-
2
set_callback(:teardown, :after, *args, &block)
-
end
-
end
-
-
1
def before_setup # :nodoc:
-
6
super
-
6
run_callbacks :setup
-
end
-
-
1
def after_teardown # :nodoc:
-
6
begin
-
6
run_callbacks :teardown
-
rescue => e
-
self.failures << Minitest::UnexpectedError.new(e)
-
end
-
-
6
super
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActiveSupport
-
1
module Testing
-
1
module Stream #:nodoc:
-
1
private
-
-
1
def silence_stream(stream)
-
old_stream = stream.dup
-
stream.reopen(IO::NULL)
-
stream.sync = true
-
yield
-
ensure
-
stream.reopen(old_stream)
-
old_stream.close
-
end
-
-
1
def quietly
-
silence_stream(STDOUT) do
-
silence_stream(STDERR) do
-
yield
-
end
-
end
-
end
-
-
1
def capture(stream)
-
stream = stream.to_s
-
captured_stream = Tempfile.new(stream)
-
stream_io = eval("$#{stream}")
-
origin_stream = stream_io.dup
-
stream_io.reopen(captured_stream)
-
-
yield
-
-
stream_io.rewind
-
return captured_stream.read
-
ensure
-
captured_stream.close
-
captured_stream.unlink
-
stream_io.reopen(origin_stream)
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ActiveSupport
-
1
module Testing
-
# Logs a "PostsControllerTest: test name" heading before each test to
-
# make test.log easier to search and follow along with.
-
1
module TaggedLogging #:nodoc:
-
1
attr_writer :tagged_logger
-
-
1
def before_setup
-
6
if tagged_logger && tagged_logger.info?
-
6
heading = "#{self.class}: #{name}"
-
6
divider = "-" * heading.size
-
6
tagged_logger.info divider
-
6
tagged_logger.info heading
-
6
tagged_logger.info divider
-
end
-
6
super
-
end
-
-
1
private
-
1
def tagged_logger
-
30
@tagged_logger ||= (defined?(Rails.logger) && Rails.logger)
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_support/core_ext/module/redefine_method"
-
1
require "active_support/core_ext/string/strip" # for strip_heredoc
-
1
require "active_support/core_ext/time/calculations"
-
1
require "concurrent/map"
-
-
1
module ActiveSupport
-
1
module Testing
-
1
class SimpleStubs # :nodoc:
-
1
Stub = Struct.new(:object, :method_name, :original_method)
-
-
1
def initialize
-
6
@stubs = Concurrent::Map.new { |h, k| h[k] = {} }
-
end
-
-
1
def stub_object(object, method_name, &block)
-
if stub = stubbing(object, method_name)
-
unstub_object(stub)
-
end
-
-
new_name = "__simple_stub__#{method_name}"
-
-
@stubs[object.object_id][method_name] = Stub.new(object, method_name, new_name)
-
-
object.singleton_class.send :alias_method, new_name, method_name
-
object.define_singleton_method(method_name, &block)
-
end
-
-
1
def unstub_all!
-
6
@stubs.each_value do |object_stubs|
-
object_stubs.each_value do |stub|
-
unstub_object(stub)
-
end
-
end
-
6
@stubs.clear
-
end
-
-
1
def stubbing(object, method_name)
-
@stubs[object.object_id][method_name]
-
end
-
-
1
private
-
-
1
def unstub_object(stub)
-
singleton_class = stub.object.singleton_class
-
singleton_class.send :silence_redefinition_of_method, stub.method_name
-
singleton_class.send :alias_method, stub.method_name, stub.original_method
-
singleton_class.send :undef_method, stub.original_method
-
end
-
end
-
-
# Contains helpers that help you test passage of time.
-
1
module TimeHelpers
-
1
def after_teardown
-
6
travel_back
-
6
super
-
end
-
-
# Changes current time to the time in the future or in the past by a given time difference by
-
# stubbing +Time.now+, +Date.today+, and +DateTime.now+. The stubs are automatically removed
-
# at the end of the test.
-
#
-
# Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
-
# travel 1.day
-
# Time.current # => Sun, 10 Nov 2013 15:34:49 EST -05:00
-
# Date.current # => Sun, 10 Nov 2013
-
# DateTime.current # => Sun, 10 Nov 2013 15:34:49 -0500
-
#
-
# This method also accepts a block, which will return the current time back to its original
-
# state at the end of the block:
-
#
-
# Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
-
# travel 1.day do
-
# User.create.created_at # => Sun, 10 Nov 2013 15:34:49 EST -05:00
-
# end
-
# Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
-
1
def travel(duration, &block)
-
travel_to Time.now + duration, &block
-
end
-
-
# Changes current time to the given time by stubbing +Time.now+,
-
# +Date.today+, and +DateTime.now+ to return the time or date passed into this method.
-
# The stubs are automatically removed at the end of the test.
-
#
-
# Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
-
# travel_to Time.zone.local(2004, 11, 24, 01, 04, 44)
-
# Time.current # => Wed, 24 Nov 2004 01:04:44 EST -05:00
-
# Date.current # => Wed, 24 Nov 2004
-
# DateTime.current # => Wed, 24 Nov 2004 01:04:44 -0500
-
#
-
# Dates are taken as their timestamp at the beginning of the day in the
-
# application time zone. <tt>Time.current</tt> returns said timestamp,
-
# and <tt>Time.now</tt> its equivalent in the system time zone. Similarly,
-
# <tt>Date.current</tt> returns a date equal to the argument, and
-
# <tt>Date.today</tt> the date according to <tt>Time.now</tt>, which may
-
# be different. (Note that you rarely want to deal with <tt>Time.now</tt>,
-
# or <tt>Date.today</tt>, in order to honor the application time zone
-
# please always use <tt>Time.current</tt> and <tt>Date.current</tt>.)
-
#
-
# Note that the usec for the time passed will be set to 0 to prevent rounding
-
# errors with external services, like MySQL (which will round instead of floor,
-
# leading to off-by-one-second errors).
-
#
-
# This method also accepts a block, which will return the current time back to its original
-
# state at the end of the block:
-
#
-
# Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
-
# travel_to Time.zone.local(2004, 11, 24, 01, 04, 44) do
-
# Time.current # => Wed, 24 Nov 2004 01:04:44 EST -05:00
-
# end
-
# Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
-
1
def travel_to(date_or_time)
-
if block_given? && simple_stubs.stubbing(Time, :now)
-
travel_to_nested_block_call = <<-MSG.strip_heredoc
-
-
Calling `travel_to` with a block, when we have previously already made a call to `travel_to`, can lead to confusing time stubbing.
-
-
Instead of:
-
-
travel_to 2.days.from_now do
-
# 2 days from today
-
travel_to 3.days.from_now do
-
# 5 days from today
-
end
-
end
-
-
preferred way to achieve above is:
-
-
travel 2.days do
-
# 2 days from today
-
end
-
-
travel 5.days do
-
# 5 days from today
-
end
-
-
MSG
-
raise travel_to_nested_block_call
-
end
-
-
if date_or_time.is_a?(Date) && !date_or_time.is_a?(DateTime)
-
now = date_or_time.midnight.to_time
-
else
-
now = date_or_time.to_time.change(usec: 0)
-
end
-
-
simple_stubs.stub_object(Time, :now) { at(now.to_i) }
-
simple_stubs.stub_object(Date, :today) { jd(now.to_date.jd) }
-
simple_stubs.stub_object(DateTime, :now) { jd(now.to_date.jd, now.hour, now.min, now.sec, Rational(now.utc_offset, 86400)) }
-
-
if block_given?
-
begin
-
yield
-
ensure
-
travel_back
-
end
-
end
-
end
-
-
# Returns the current time back to its original state, by removing the stubs added by
-
# +travel+ and +travel_to+.
-
#
-
# Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
-
# travel_to Time.zone.local(2004, 11, 24, 01, 04, 44)
-
# Time.current # => Wed, 24 Nov 2004 01:04:44 EST -05:00
-
# travel_back
-
# Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
-
1
def travel_back
-
6
simple_stubs.unstub_all!
-
end
-
-
# Calls +travel_to+ with +Time.now+.
-
#
-
# Time.current # => Sun, 09 Jul 2017 15:34:49 EST -05:00
-
# freeze_time
-
# sleep(1)
-
# Time.current # => Sun, 09 Jul 2017 15:34:49 EST -05:00
-
#
-
# This method also accepts a block, which will return the current time back to its original
-
# state at the end of the block:
-
#
-
# Time.current # => Sun, 09 Jul 2017 15:34:49 EST -05:00
-
# freeze_time do
-
# sleep(1)
-
# User.create.created_at # => Sun, 09 Jul 2017 15:34:49 EST -05:00
-
# end
-
# Time.current # => Sun, 09 Jul 2017 15:34:50 EST -05:00
-
1
def freeze_time(&block)
-
travel_to Time.now, &block
-
end
-
-
1
private
-
-
1
def simple_stubs
-
6
@simple_stubs ||= SimpleStubs.new
-
end
-
end
-
end
-
end
-
1
require 'active_support/concern'
-
-
1
class GlobalID
-
1
module Identification
-
1
extend ActiveSupport::Concern
-
-
1
def to_global_id(options = {})
-
GlobalID.create(self, options)
-
end
-
1
alias to_gid to_global_id
-
-
1
def to_gid_param(options = {})
-
to_global_id(options).to_param
-
end
-
-
1
def to_signed_global_id(options = {})
-
SignedGlobalID.create(self, options)
-
end
-
1
alias to_sgid to_signed_global_id
-
-
1
def to_sgid_param(options = {})
-
to_signed_global_id(options).to_param
-
end
-
end
-
end
-
1
require 'global_id'
-
1
require 'active_support/message_verifier'
-
1
require 'time'
-
-
1
class SignedGlobalID < GlobalID
-
1
class ExpiredMessage < StandardError; end
-
-
1
class << self
-
1
attr_accessor :verifier
-
-
1
def parse(sgid, options = {})
-
super verify(sgid.to_s, options), options
-
end
-
-
# Grab the verifier from options and fall back to SignedGlobalID.verifier.
-
# Raise ArgumentError if neither is available.
-
1
def pick_verifier(options)
-
options.fetch :verifier do
-
verifier || raise(ArgumentError, 'Pass a `verifier:` option with an `ActiveSupport::MessageVerifier` instance, or set a default SignedGlobalID.verifier.')
-
end
-
end
-
-
1
attr_accessor :expires_in
-
-
1
DEFAULT_PURPOSE = "default"
-
-
1
def pick_purpose(options)
-
options.fetch :for, DEFAULT_PURPOSE
-
end
-
-
1
private
-
1
def verify(sgid, options)
-
metadata = pick_verifier(options).verify(sgid)
-
-
raise_if_expired(metadata['expires_at'])
-
-
metadata['gid'] if pick_purpose(options) == metadata['purpose']
-
rescue ActiveSupport::MessageVerifier::InvalidSignature, ExpiredMessage
-
nil
-
end
-
-
1
def raise_if_expired(expires_at)
-
if expires_at && Time.now.utc > Time.iso8601(expires_at)
-
raise ExpiredMessage, 'This signed global id has expired.'
-
end
-
end
-
end
-
-
1
attr_reader :verifier, :purpose, :expires_at
-
-
1
def initialize(gid, options = {})
-
super
-
@verifier = self.class.pick_verifier(options)
-
@purpose = self.class.pick_purpose(options)
-
@expires_at = pick_expiration(options)
-
end
-
-
1
def to_s
-
@sgid ||= @verifier.generate(to_h)
-
end
-
1
alias to_param to_s
-
-
1
def to_h
-
# Some serializers decodes symbol keys to symbols, others to strings.
-
# Using string keys remedies that.
-
{ 'gid' => @uri.to_s, 'purpose' => purpose, 'expires_at' => encoded_expiration }
-
end
-
-
1
def ==(other)
-
super && @purpose == other.purpose
-
end
-
-
1
private
-
1
def encoded_expiration
-
expires_at.utc.iso8601(3) if expires_at
-
end
-
-
1
def pick_expiration(options)
-
return options[:expires_at] if options.key?(:expires_at)
-
-
if expires_in = options.fetch(:expires_in) { self.class.expires_in }
-
expires_in.from_now
-
end
-
end
-
end
-
1
require 'active_support'
-
1
require 'active_support/message_verifier'
-
-
1
class GlobalID
-
1
class Verifier < ActiveSupport::MessageVerifier
-
1
private
-
1
def encode(data)
-
::Base64.urlsafe_encode64(data)
-
end
-
-
1
def decode(data)
-
::Base64.urlsafe_decode64(data)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module I18n
-
1
module Backend
-
1
autoload :Base, 'i18n/backend/base'
-
1
autoload :InterpolationCompiler, 'i18n/backend/interpolation_compiler'
-
1
autoload :Cache, 'i18n/backend/cache'
-
1
autoload :CacheFile, 'i18n/backend/cache_file'
-
1
autoload :Cascade, 'i18n/backend/cascade'
-
1
autoload :Chain, 'i18n/backend/chain'
-
1
autoload :Fallbacks, 'i18n/backend/fallbacks'
-
1
autoload :Flatten, 'i18n/backend/flatten'
-
1
autoload :Gettext, 'i18n/backend/gettext'
-
1
autoload :KeyValue, 'i18n/backend/key_value'
-
1
autoload :Memoize, 'i18n/backend/memoize'
-
1
autoload :Metadata, 'i18n/backend/metadata'
-
1
autoload :Pluralization, 'i18n/backend/pluralization'
-
1
autoload :Simple, 'i18n/backend/simple'
-
1
autoload :Transliterator, 'i18n/backend/transliterator'
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require 'yaml'
-
1
require 'json'
-
1
require 'i18n/core_ext/hash'
-
-
1
module I18n
-
1
module Backend
-
1
module Base
-
1
using I18n::HashRefinements
-
1
include I18n::Backend::Transliterator
-
-
# Accepts a list of paths to translation files. Loads translations from
-
# plain Ruby (*.rb), YAML files (*.yml), or JSON files (*.json). See #load_rb, #load_yml, and #load_json
-
# for details.
-
1
def load_translations(*filenames)
-
filenames = I18n.load_path if filenames.empty?
-
filenames.flatten.each { |filename| load_file(filename) }
-
end
-
-
# This method receives a locale, a data hash and options for storing translations.
-
# Should be implemented
-
1
def store_translations(locale, data, options = EMPTY_HASH)
-
raise NotImplementedError
-
end
-
-
1
def translate(locale, key, options = EMPTY_HASH)
-
raise I18n::ArgumentError if (key.is_a?(String) || key.is_a?(Symbol)) && key.empty?
-
raise InvalidLocale.new(locale) unless locale
-
return nil if key.nil? && !options.key?(:default)
-
-
entry = lookup(locale, key, options[:scope], options) unless key.nil?
-
-
if entry.nil? && options.key?(:default)
-
entry = default(locale, key, options[:default], options)
-
else
-
entry = resolve(locale, key, entry, options)
-
end
-
-
count = options[:count]
-
-
if entry.nil? && (subtrees? || !count)
-
if (options.key?(:default) && !options[:default].nil?) || !options.key?(:default)
-
throw(:exception, I18n::MissingTranslation.new(locale, key, options))
-
end
-
end
-
-
entry = entry.dup if entry.is_a?(String)
-
entry = pluralize(locale, entry, count) if count
-
-
if entry.nil? && !subtrees?
-
throw(:exception, I18n::MissingTranslation.new(locale, key, options))
-
end
-
-
deep_interpolation = options[:deep_interpolation]
-
values = options.except(*RESERVED_KEYS)
-
if values
-
entry = if deep_interpolation
-
deep_interpolate(locale, entry, values)
-
else
-
interpolate(locale, entry, values)
-
end
-
end
-
entry
-
end
-
-
1
def exists?(locale, key)
-
lookup(locale, key) != nil
-
end
-
-
# Acts the same as +strftime+, but uses a localized version of the
-
# format string. Takes a key from the date/time formats translations as
-
# a format argument (<em>e.g.</em>, <tt>:short</tt> in <tt>:'date.formats'</tt>).
-
1
def localize(locale, object, format = :default, options = EMPTY_HASH)
-
if object.nil? && options.include?(:default)
-
return options[:default]
-
end
-
raise ArgumentError, "Object must be a Date, DateTime or Time object. #{object.inspect} given." unless object.respond_to?(:strftime)
-
-
if Symbol === format
-
key = format
-
type = object.respond_to?(:sec) ? 'time' : 'date'
-
options = options.merge(:raise => true, :object => object, :locale => locale)
-
format = I18n.t(:"#{type}.formats.#{key}", options)
-
end
-
-
format = translate_localization_format(locale, object, format, options)
-
object.strftime(format)
-
end
-
-
# Returns an array of locales for which translations are available
-
# ignoring the reserved translation meta data key :i18n.
-
1
def available_locales
-
raise NotImplementedError
-
end
-
-
1
def reload!
-
3
eager_load! if eager_loaded?
-
end
-
-
1
def eager_load!
-
@eager_loaded = true
-
end
-
-
1
protected
-
-
1
def eager_loaded?
-
3
@eager_loaded ||= false
-
end
-
-
# The method which actually looks up for the translation in the store.
-
1
def lookup(locale, key, scope = [], options = EMPTY_HASH)
-
raise NotImplementedError
-
end
-
-
1
def subtrees?
-
true
-
end
-
-
# Evaluates defaults.
-
# If given subject is an Array, it walks the array and returns the
-
# first translation that can be resolved. Otherwise it tries to resolve
-
# the translation directly.
-
1
def default(locale, object, subject, options = EMPTY_HASH)
-
options = options.dup.reject { |key, value| key == :default }
-
case subject
-
when Array
-
subject.each do |item|
-
result = resolve(locale, object, item, options)
-
return result unless result.nil?
-
end and nil
-
else
-
resolve(locale, object, subject, options)
-
end
-
end
-
-
# Resolves a translation.
-
# If the given subject is a Symbol, it will be translated with the
-
# given options. If it is a Proc then it will be evaluated. All other
-
# subjects will be returned directly.
-
1
def resolve(locale, object, subject, options = EMPTY_HASH)
-
return subject if options[:resolve] == false
-
result = catch(:exception) do
-
case subject
-
when Symbol
-
I18n.translate(subject, options.merge(:locale => locale, :throw => true))
-
when Proc
-
date_or_time = options.delete(:object) || object
-
resolve(locale, object, subject.call(date_or_time, options))
-
else
-
subject
-
end
-
end
-
result unless result.is_a?(MissingTranslation)
-
end
-
-
# Picks a translation from a pluralized mnemonic subkey according to English
-
# pluralization rules :
-
# - It will pick the :one subkey if count is equal to 1.
-
# - It will pick the :other subkey otherwise.
-
# - It will pick the :zero subkey in the special case where count is
-
# equal to 0 and there is a :zero subkey present. This behaviour is
-
# not standard with regards to the CLDR pluralization rules.
-
# Other backends can implement more flexible or complex pluralization rules.
-
1
def pluralize(locale, entry, count)
-
return entry unless entry.is_a?(Hash) && count
-
-
key = pluralization_key(entry, count)
-
raise InvalidPluralizationData.new(entry, count, key) unless entry.has_key?(key)
-
entry[key]
-
end
-
-
# Interpolates values into a given subject.
-
#
-
# if the given subject is a string then:
-
# method interpolates "file %{file} opened by %%{user}", :file => 'test.txt', :user => 'Mr. X'
-
# # => "file test.txt opened by %{user}"
-
#
-
# if the given subject is an array then:
-
# each element of the array is recursively interpolated (until it finds a string)
-
# method interpolates ["yes, %{user}", ["maybe no, %{user}, "no, %{user}"]], :user => "bartuz"
-
# # => "["yes, bartuz",["maybe no, bartuz", "no, bartuz"]]"
-
1
def interpolate(locale, subject, values = EMPTY_HASH)
-
return subject if values.empty?
-
-
case subject
-
when ::String then I18n.interpolate(subject, values)
-
when ::Array then subject.map { |element| interpolate(locale, element, values) }
-
else
-
subject
-
end
-
end
-
-
# Deep interpolation
-
#
-
# deep_interpolate { people: { ann: "Ann is %{ann}", john: "John is %{john}" } },
-
# ann: 'good', john: 'big'
-
# #=> { people: { ann: "Ann is good", john: "John is big" } }
-
1
def deep_interpolate(locale, data, values = EMPTY_HASH)
-
return data if values.empty?
-
-
case data
-
when ::String
-
I18n.interpolate(data, values)
-
when ::Hash
-
data.each_with_object({}) do |(k, v), result|
-
result[k] = deep_interpolate(locale, v, values)
-
end
-
when ::Array
-
data.map do |v|
-
deep_interpolate(locale, v, values)
-
end
-
else
-
data
-
end
-
end
-
-
# Loads a single translations file by delegating to #load_rb or
-
# #load_yml depending on the file extension and directly merges the
-
# data to the existing translations. Raises I18n::UnknownFileType
-
# for all other file extensions.
-
1
def load_file(filename)
-
type = File.extname(filename).tr('.', '').downcase
-
raise UnknownFileType.new(type, filename) unless respond_to?(:"load_#{type}", true)
-
data = send(:"load_#{type}", filename)
-
unless data.is_a?(Hash)
-
raise InvalidLocaleData.new(filename, 'expects it to return a hash, but does not')
-
end
-
data.each { |locale, d| store_translations(locale, d || {}) }
-
end
-
-
# Loads a plain Ruby translations file. eval'ing the file must yield
-
# a Hash containing translation data with locales as toplevel keys.
-
1
def load_rb(filename)
-
eval(IO.read(filename), binding, filename)
-
end
-
-
# Loads a YAML translations file. The data must have locales as
-
# toplevel keys.
-
1
def load_yml(filename)
-
begin
-
YAML.load_file(filename)
-
rescue TypeError, ScriptError, StandardError => e
-
raise InvalidLocaleData.new(filename, e.inspect)
-
end
-
end
-
1
alias_method :load_yaml, :load_yml
-
-
# Loads a JSON translations file. The data must have locales as
-
# toplevel keys.
-
1
def load_json(filename)
-
begin
-
::JSON.parse(File.read(filename))
-
rescue TypeError, StandardError => e
-
raise InvalidLocaleData.new(filename, e.inspect)
-
end
-
end
-
-
1
def translate_localization_format(locale, object, format, options)
-
format.to_s.gsub(/%(|\^)[aAbBpP]/) do |match|
-
case match
-
when '%a' then I18n.t!(:"date.abbr_day_names", :locale => locale, :format => format)[object.wday]
-
when '%^a' then I18n.t!(:"date.abbr_day_names", :locale => locale, :format => format)[object.wday].upcase
-
when '%A' then I18n.t!(:"date.day_names", :locale => locale, :format => format)[object.wday]
-
when '%^A' then I18n.t!(:"date.day_names", :locale => locale, :format => format)[object.wday].upcase
-
when '%b' then I18n.t!(:"date.abbr_month_names", :locale => locale, :format => format)[object.mon]
-
when '%^b' then I18n.t!(:"date.abbr_month_names", :locale => locale, :format => format)[object.mon].upcase
-
when '%B' then I18n.t!(:"date.month_names", :locale => locale, :format => format)[object.mon]
-
when '%^B' then I18n.t!(:"date.month_names", :locale => locale, :format => format)[object.mon].upcase
-
when '%p' then I18n.t!(:"time.#{object.hour < 12 ? :am : :pm}", :locale => locale, :format => format).upcase if object.respond_to? :hour
-
when '%P' then I18n.t!(:"time.#{object.hour < 12 ? :am : :pm}", :locale => locale, :format => format).downcase if object.respond_to? :hour
-
end
-
end
-
rescue MissingTranslationData => e
-
e.message
-
end
-
-
1
def pluralization_key(entry, count)
-
key = :zero if count == 0 && entry.has_key?(:zero)
-
key ||= count == 1 ? :one : :other
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require 'i18n/backend/base'
-
-
1
module I18n
-
1
module Backend
-
# A simple backend that reads translations from YAML files and stores them in
-
# an in-memory hash. Relies on the Base backend.
-
#
-
# The implementation is provided by a Implementation module allowing to easily
-
# extend Simple backend's behavior by including modules. E.g.:
-
#
-
# module I18n::Backend::Pluralization
-
# def pluralize(*args)
-
# # extended pluralization logic
-
# super
-
# end
-
# end
-
#
-
# I18n::Backend::Simple.include(I18n::Backend::Pluralization)
-
1
class Simple
-
1
using I18n::HashRefinements
-
-
3
(class << self; self; end).class_eval { public :include }
-
-
1
module Implementation
-
1
include Base
-
-
1
def initialized?
-
@initialized ||= false
-
end
-
-
# Stores translations for the given locale in memory.
-
# This uses a deep merge for the translations hash, so existing
-
# translations will be overwritten by new ones only at the deepest
-
# level of the hash.
-
1
def store_translations(locale, data, options = EMPTY_HASH)
-
if I18n.enforce_available_locales &&
-
I18n.available_locales_initialized? &&
-
!I18n.available_locales.include?(locale.to_sym) &&
-
!I18n.available_locales.include?(locale.to_s)
-
return data
-
end
-
locale = locale.to_sym
-
translations[locale] ||= {}
-
data = data.deep_symbolize_keys
-
translations[locale].deep_merge!(data)
-
end
-
-
# Get available locales from the translations hash
-
1
def available_locales
-
init_translations unless initialized?
-
translations.inject([]) do |locales, (locale, data)|
-
locales << locale unless data.size <= 1 && (data.empty? || data.has_key?(:i18n))
-
locales
-
end
-
end
-
-
# Clean up translations hash and set initialized to false on reload!
-
1
def reload!
-
3
@initialized = false
-
3
@translations = nil
-
3
super
-
end
-
-
1
def eager_load!
-
init_translations unless initialized?
-
super
-
end
-
-
1
def translations(do_init: false)
-
# To avoid returning empty translations,
-
# call `init_translations`
-
init_translations if do_init && !initialized?
-
-
@translations ||= {}
-
end
-
-
1
protected
-
-
1
def init_translations
-
load_translations
-
@initialized = true
-
end
-
-
# Looks up a translation from the translations hash. Returns nil if
-
# either key is nil, or locale, scope or key do not exist as a key in the
-
# nested translations hash. Splits keys or scopes containing dots
-
# into multiple keys, i.e. <tt>currency.format</tt> is regarded the same as
-
# <tt>%w(currency format)</tt>.
-
1
def lookup(locale, key, scope = [], options = EMPTY_HASH)
-
init_translations unless initialized?
-
keys = I18n.normalize_keys(locale, key, scope, options[:separator])
-
-
keys.inject(translations) do |result, _key|
-
return nil unless result.is_a?(Hash)
-
unless result.has_key?(_key)
-
_key = _key.to_s.to_sym
-
return nil unless result.has_key?(_key)
-
end
-
result = result[_key]
-
result = resolve(locale, _key, result, options.merge(:scope => nil)) if result.is_a?(Symbol)
-
result
-
end
-
end
-
end
-
-
1
include Implementation
-
end
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
-
1
module I18n
-
1
module Backend
-
1
module Transliterator
-
1
DEFAULT_REPLACEMENT_CHAR = "?"
-
-
# Given a locale and a UTF-8 string, return the locale's ASCII
-
# approximation for the string.
-
1
def transliterate(locale, string, replacement = nil)
-
@transliterators ||= {}
-
@transliterators[locale] ||= Transliterator.get I18n.t(:'i18n.transliterate.rule',
-
:locale => locale, :resolve => false, :default => {})
-
@transliterators[locale].transliterate(string, replacement)
-
end
-
-
# Get a transliterator instance.
-
1
def self.get(rule = nil)
-
if !rule || rule.kind_of?(Hash)
-
HashTransliterator.new(rule)
-
elsif rule.kind_of? Proc
-
ProcTransliterator.new(rule)
-
else
-
raise I18n::ArgumentError, "Transliteration rule must be a proc or a hash."
-
end
-
end
-
-
# A transliterator which accepts a Proc as its transliteration rule.
-
1
class ProcTransliterator
-
1
def initialize(rule)
-
@rule = rule
-
end
-
-
1
def transliterate(string, replacement = nil)
-
@rule.call(string)
-
end
-
end
-
-
# A transliterator which accepts a Hash of characters as its translation
-
# rule.
-
1
class HashTransliterator
-
DEFAULT_APPROXIMATIONS = {
-
"��"=>"A", "��"=>"A", "��"=>"A", "��"=>"A", "��"=>"A", "��"=>"A", "��"=>"AE",
-
"��"=>"C", "��"=>"E", "��"=>"E", "��"=>"E", "��"=>"E", "��"=>"I", "��"=>"I",
-
"��"=>"I", "��"=>"I", "��"=>"D", "��"=>"N", "��"=>"O", "��"=>"O", "��"=>"O",
-
"��"=>"O", "��"=>"O", "��"=>"x", "��"=>"O", "��"=>"U", "��"=>"U", "��"=>"U",
-
"��"=>"U", "��"=>"Y", "��"=>"Th", "��"=>"ss", "��"=>"a", "��"=>"a", "��"=>"a",
-
"��"=>"a", "��"=>"a", "��"=>"a", "��"=>"ae", "��"=>"c", "��"=>"e", "��"=>"e",
-
"��"=>"e", "��"=>"e", "��"=>"i", "��"=>"i", "��"=>"i", "��"=>"i", "��"=>"d",
-
"��"=>"n", "��"=>"o", "��"=>"o", "��"=>"o", "��"=>"o", "��"=>"o", "��"=>"o",
-
"��"=>"u", "��"=>"u", "��"=>"u", "��"=>"u", "��"=>"y", "��"=>"th", "��"=>"y",
-
"��"=>"A", "��"=>"a", "��"=>"A", "��"=>"a", "��"=>"A", "��"=>"a", "��"=>"C",
-
"��"=>"c", "��"=>"C", "��"=>"c", "��"=>"C", "��"=>"c", "��"=>"C", "��"=>"c",
-
"��"=>"D", "��"=>"d", "��"=>"D", "��"=>"d", "��"=>"E", "��"=>"e", "��"=>"E",
-
"��"=>"e", "��"=>"E", "��"=>"e", "��"=>"E", "��"=>"e", "��"=>"E", "��"=>"e",
-
"��"=>"G", "��"=>"g", "��"=>"G", "��"=>"g", "��"=>"G", "��"=>"g", "��"=>"G",
-
"��"=>"g", "��"=>"H", "��"=>"h", "��"=>"H", "��"=>"h", "��"=>"I", "��"=>"i",
-
"��"=>"I", "��"=>"i", "��"=>"I", "��"=>"i", "��"=>"I", "��"=>"i", "��"=>"I",
-
"��"=>"i", "��"=>"IJ", "��"=>"ij", "��"=>"J", "��"=>"j", "��"=>"K", "��"=>"k",
-
"��"=>"k", "��"=>"L", "��"=>"l", "��"=>"L", "��"=>"l", "��"=>"L", "��"=>"l",
-
"��"=>"L", "��"=>"l", "��"=>"L", "��"=>"l", "��"=>"N", "��"=>"n", "��"=>"N",
-
"��"=>"n", "��"=>"N", "��"=>"n", "��"=>"'n", "��"=>"NG", "��"=>"ng",
-
"��"=>"O", "��"=>"o", "��"=>"O", "��"=>"o", "��"=>"O", "��"=>"o", "��"=>"OE",
-
"��"=>"oe", "��"=>"R", "��"=>"r", "��"=>"R", "��"=>"r", "��"=>"R", "��"=>"r",
-
"��"=>"S", "��"=>"s", "��"=>"S", "��"=>"s", "��"=>"S", "��"=>"s", "��"=>"S",
-
"��"=>"s", "��"=>"T", "��"=>"t", "��"=>"T", "��"=>"t", "��"=>"T", "��"=>"t",
-
"��"=>"U", "��"=>"u", "��"=>"U", "��"=>"u", "��"=>"U", "��"=>"u", "��"=>"U",
-
"��"=>"u", "��"=>"U", "��"=>"u", "��"=>"U", "��"=>"u", "��"=>"W", "��"=>"w",
-
"��"=>"Y", "��"=>"y", "��"=>"Y", "��"=>"Z", "��"=>"z", "��"=>"Z", "��"=>"z",
-
"��"=>"Z", "��"=>"z"
-
}.freeze
-
-
1
def initialize(rule = nil)
-
@rule = rule
-
add_default_approximations
-
add rule if rule
-
end
-
-
1
def transliterate(string, replacement = nil)
-
replacement ||= DEFAULT_REPLACEMENT_CHAR
-
string.gsub(/[^\x00-\x7f]/u) do |char|
-
approximations[char] || replacement
-
end
-
end
-
-
1
private
-
-
1
def approximations
-
@approximations ||= {}
-
end
-
-
1
def add_default_approximations
-
DEFAULT_APPROXIMATIONS.each do |key, value|
-
approximations[key] = value
-
end
-
end
-
-
# Add transliteration rules to the approximations hash.
-
1
def add(hash)
-
hash.each do |key, value|
-
approximations[key.to_s] = value.to_s
-
end
-
end
-
end
-
end
-
end
-
end
-
1
module I18n
-
1
module HashRefinements
-
1
refine Hash do
-
1
using I18n::HashRefinements
-
1
def except(*keys)
-
dup.except!(*keys)
-
end
-
-
1
def except!(*keys)
-
keys.each { |key| delete(key) }
-
self
-
end
-
-
1
def deep_symbolize_keys
-
each_with_object({}) do |(key, value), result|
-
result[symbolize_key(key)] = deep_symbolize_keys_in_object(value)
-
result
-
end
-
end
-
-
# deep_merge_hash! by Stefan Rusterholz, see http://www.ruby-forum.com/topic/142809
-
1
def deep_merge!(data)
-
merger = lambda do |_key, v1, v2|
-
Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : v2
-
end
-
merge!(data, &merger)
-
end
-
-
1
def symbolize_key(key)
-
key.respond_to?(:to_sym) ? key.to_sym : key
-
end
-
-
1
private
-
-
1
def deep_symbolize_keys_in_object(value)
-
case value
-
when Hash
-
value.deep_symbolize_keys
-
when Array
-
value.map { |e| deep_symbolize_keys_in_object(e) }
-
else
-
value
-
end
-
end
-
end
-
end
-
end
-
1
require 'jbuilder/jbuilder'
-
-
1
dependency_tracker = false
-
-
1
begin
-
1
require 'action_view'
-
1
require 'action_view/dependency_tracker'
-
1
dependency_tracker = ::ActionView::DependencyTracker
-
rescue LoadError
-
begin
-
require 'cache_digests'
-
dependency_tracker = ::CacheDigests::DependencyTracker
-
rescue LoadError
-
end
-
end
-
-
1
if dependency_tracker
-
1
class Jbuilder
-
1
module DependencyTrackerMethods
-
# Matches:
-
# json.partial! "messages/message"
-
# json.partial!('messages/message')
-
#
-
1
DIRECT_RENDERS = /
-
\w+\.partial! # json.partial!
-
\(?\s* # optional parenthesis
-
(['"])([^'"]+)\1 # quoted value
-
/x
-
-
# Matches:
-
# json.partial! partial: "comments/comment"
-
# json.comments @post.comments, partial: "comments/comment", as: :comment
-
# json.array! @posts, partial: "posts/post", as: :post
-
# = render partial: "account"
-
#
-
1
INDIRECT_RENDERS = /
-
(?::partial\s*=>|partial:) # partial: or :partial =>
-
\s* # optional whitespace
-
(['"])([^'"]+)\1 # quoted value
-
/x
-
-
1
def dependencies
-
direct_dependencies + indirect_dependencies + explicit_dependencies
-
end
-
-
1
private
-
-
1
def direct_dependencies
-
source.scan(DIRECT_RENDERS).map(&:second)
-
end
-
-
1
def indirect_dependencies
-
source.scan(INDIRECT_RENDERS).map(&:second)
-
end
-
end
-
end
-
-
1
::Jbuilder::DependencyTracker = Class.new(dependency_tracker::ERBTracker)
-
1
::Jbuilder::DependencyTracker.send :include, ::Jbuilder::DependencyTrackerMethods
-
1
dependency_tracker.register_tracker :jbuilder, ::Jbuilder::DependencyTracker
-
end
-
1
require 'jdbc/postgres/version'
-
-
1
module Jdbc
-
1
module Postgres
-
-
1
def self.driver_jar
-
2
version_jre_version = DRIVER_VERSION.split( '.' )
-
2
version = jre_version
-
2
version_jre_version << (version ? ".jre#{version}" : '')
-
2
'postgresql-%s.%s.%s%s.jar' % version_jre_version
-
end
-
-
1
def self.load_driver(method = :load)
-
2
send method, driver_jar
-
rescue LoadError => e
-
if (version = jre_version) && version < 6
-
warn "failed to load postgresql (driver) jar, please note that we no longer " <<
-
"include JDBC 3.x support, on Java < 6 please use gem 'jdbc-postgres', '~> 9.2'"
-
end
-
raise e
-
end
-
-
1
def self.driver_name
-
1
'org.postgresql.Driver'
-
end
-
-
1
private
-
-
1
def self.jre_version
-
2
version = ENV_JAVA[ 'java.specification.version' ]
-
2
version = version.split('.').last.to_i # '1.7' => 7, '9' => 9
-
2
if version < 6
-
5 # not supported
-
1
elsif version == 6
-
6
-
1
elsif version == 7
-
7
-
else
-
2
nil # non-tagged X.Y.Z.jar
-
end
-
end
-
-
2
class << self; private :jre_version end
-
-
1
if defined?(JRUBY_VERSION) && # enable backwards-compat behavior :
-
1
( Java::JavaLang::Boolean.get_boolean("jdbc.driver.autoload") ||
-
Java::JavaLang::Boolean.get_boolean("jdbc.postgres.autoload") )
-
warn "autoloading JDBC driver on require 'jdbc/postgres'" if $VERBOSE
-
load_driver :require
-
end
-
end
-
1
PostgreSQL = Postgres unless const_defined?(:PostgreSQL)
-
end
-
1
module Jdbc
-
1
module Postgres
-
1
DRIVER_VERSION = '42.2.6'
-
1
VERSION = DRIVER_VERSION
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
1
module Mail # :doc:
-
-
1
require 'date'
-
1
require 'shellwords'
-
-
1
require 'uri'
-
1
require 'net/smtp'
-
1
require 'mini_mime'
-
-
1
if RUBY_VERSION <= '1.8.6'
-
begin
-
require 'tlsmail'
-
rescue LoadError
-
raise "You need to install tlsmail if you are using ruby <= 1.8.6"
-
end
-
end
-
-
1
if RUBY_VERSION >= "1.9.0"
-
1
require 'mail/version_specific/ruby_1_9'
-
1
RubyVer = Ruby19
-
else
-
require 'mail/version_specific/ruby_1_8'
-
RubyVer = Ruby18
-
end
-
-
1
require 'mail/version'
-
-
1
require 'mail/core_extensions/string'
-
1
require 'mail/core_extensions/smtp'
-
1
require 'mail/indifferent_hash'
-
-
1
require 'mail/multibyte'
-
-
1
require 'mail/constants'
-
1
require 'mail/utilities'
-
1
require 'mail/configuration'
-
-
1
@@autoloads = {}
-
1
def self.register_autoload(name, path)
-
55
@@autoloads[name] = path
-
55
autoload(name, path)
-
end
-
-
# This runs through the autoload list and explictly requires them for you.
-
# Useful when running mail in a threaded process.
-
#
-
# Usage:
-
#
-
# require 'mail'
-
# Mail.eager_autoload!
-
1
def self.eager_autoload!
-
@@autoloads.each { |_,path| require(path) }
-
end
-
-
# Autoload mail send and receive classes.
-
1
require 'mail/network'
-
-
1
require 'mail/message'
-
1
require 'mail/part'
-
1
require 'mail/header'
-
1
require 'mail/parts_list'
-
1
require 'mail/attachments_list'
-
1
require 'mail/body'
-
1
require 'mail/field'
-
1
require 'mail/field_list'
-
-
1
require 'mail/envelope'
-
-
1
register_autoload :Parsers, "mail/parsers"
-
-
# Autoload header field elements and transfer encodings.
-
1
require 'mail/elements'
-
1
require 'mail/encodings'
-
1
require 'mail/encodings/base64'
-
1
require 'mail/encodings/quoted_printable'
-
1
require 'mail/encodings/unix_to_unix'
-
-
1
require 'mail/matchers/has_sent_mail'
-
1
require 'mail/matchers/attachment_matchers.rb'
-
-
# Finally... require all the Mail.methods
-
1
require 'mail/mail'
-
end
-
# frozen_string_literal: true
-
1
module Mail
-
1
class AttachmentsList < Array
-
-
1
def initialize(parts_list)
-
@parts_list = parts_list
-
@content_disposition_type = 'attachment'
-
parts_list.map { |p|
-
if p.mime_type == 'message/rfc822'
-
Mail.new(p.body.encoded).attachments
-
elsif p.parts.empty?
-
p if p.attachment?
-
else
-
p.attachments
-
end
-
}.flatten.compact.each { |a| self << a }
-
self
-
end
-
-
1
def inline
-
@content_disposition_type = 'inline'
-
self
-
end
-
-
# Returns the attachment by filename or at index.
-
#
-
# mail.attachments['test.png'] = File.read('test.png')
-
# mail.attachments['test.jpg'] = File.read('test.jpg')
-
#
-
# mail.attachments['test.png'].filename #=> 'test.png'
-
# mail.attachments[1].filename #=> 'test.jpg'
-
1
def [](index_value)
-
if index_value.is_a?(Integer)
-
self.fetch(index_value)
-
else
-
self.select { |a| a.filename == index_value }.first
-
end
-
end
-
-
1
def []=(name, value)
-
encoded_name = Mail::Encodings.decode_encode name, :encode
-
default_values = { :content_type => "#{set_mime_type(name)}; filename=\"#{encoded_name}\"",
-
:content_transfer_encoding => "#{guess_encoding}",
-
:content_disposition => "#{@content_disposition_type}; filename=\"#{encoded_name}\"" }
-
-
if value.is_a?(Hash)
-
if path = value.delete(:filename)
-
value[:content] ||= File.open(path, 'rb') { |f| f.read }
-
end
-
-
default_values[:body] = value.delete(:content) if value[:content]
-
-
default_values[:body] = value.delete(:data) if value[:data]
-
-
encoding = value.delete(:transfer_encoding) || value.delete(:encoding)
-
if encoding
-
if Mail::Encodings.defined? encoding
-
default_values[:content_transfer_encoding] = encoding
-
else
-
raise "Do not know how to handle Content Transfer Encoding #{encoding}, please choose either quoted-printable or base64"
-
end
-
end
-
-
if value[:mime_type]
-
default_values[:content_type] = value.delete(:mime_type)
-
@mime_type = MiniMime.lookup_by_content_type(default_values[:content_type])
-
default_values[:content_transfer_encoding] ||= guess_encoding
-
end
-
-
hash = default_values.merge(value)
-
else
-
default_values[:body] = value
-
hash = default_values
-
end
-
-
if hash[:body].respond_to? :force_encoding and hash[:body].respond_to? :valid_encoding?
-
if not hash[:body].valid_encoding? and default_values[:content_transfer_encoding].downcase == "binary"
-
hash[:body] = hash[:body].dup if hash[:body].frozen?
-
hash[:body].force_encoding("BINARY")
-
end
-
end
-
-
attachment = Part.new(hash)
-
attachment.add_content_id(hash[:content_id])
-
-
@parts_list << attachment
-
end
-
-
# Uses the mime type to try and guess the encoding, if it is a binary type, or unknown, then we
-
# set it to binary, otherwise as set to plain text
-
1
def guess_encoding
-
if @mime_type && !@mime_type.binary?
-
"7bit"
-
else
-
"binary"
-
end
-
end
-
-
1
def set_mime_type(filename)
-
# Have to do this because MIME::Types is not Ruby 1.9 safe yet
-
if RUBY_VERSION >= '1.9'
-
filename = filename.encode(Encoding::UTF_8) if filename.respond_to?(:encode)
-
end
-
-
@mime_type = MiniMime.lookup_by_filename(filename)
-
@mime_type && @mime_type.content_type
-
end
-
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
1
module Mail
-
-
# = Body
-
#
-
# The body is where the text of the email is stored. Mail treats the body
-
# as a single object. The body itself has no information about boundaries
-
# used in the MIME standard, it just looks at its content as either a single
-
# block of text, or (if it is a multipart message) as an array of blocks of text.
-
#
-
# A body has to be told to split itself up into a multipart message by calling
-
# #split with the correct boundary. This is because the body object has no way
-
# of knowing what the correct boundary is for itself (there could be many
-
# boundaries in a body in the case of a nested MIME text).
-
#
-
# Once split is called, Mail::Body will slice itself up on this boundary,
-
# assigning anything that appears before the first part to the preamble, and
-
# anything that appears after the closing boundary to the epilogue, then
-
# each part gets initialized into a Mail::Part object.
-
#
-
# The boundary that is used to split up the Body is also stored in the Body
-
# object for use on encoding itself back out to a string. You can
-
# overwrite this if it needs to be changed.
-
#
-
# On encoding, the body will return the preamble, then each part joined by
-
# the boundary, followed by a closing boundary string and then the epilogue.
-
1
class Body
-
-
1
def initialize(string = '')
-
@boundary = nil
-
@preamble = nil
-
@epilogue = nil
-
@charset = nil
-
@part_sort_order = [ "text/plain", "text/enriched", "text/html", "multipart/alternative" ]
-
@parts = Mail::PartsList.new
-
if Utilities.blank?(string)
-
@raw_source = ''
-
else
-
# Do join first incase we have been given an Array in Ruby 1.9
-
if string.respond_to?(:join)
-
@raw_source = ::Mail::Utilities.to_crlf(string.join(''))
-
elsif string.respond_to?(:to_s)
-
@raw_source = ::Mail::Utilities.to_crlf(string.to_s)
-
else
-
raise "You can only assign a string or an object that responds_to? :join or :to_s to a body."
-
end
-
end
-
@encoding = default_encoding
-
set_charset
-
end
-
-
# Matches this body with another body. Also matches the decoded value of this
-
# body with a string.
-
#
-
# Examples:
-
#
-
# body = Mail::Body.new('The body')
-
# body == body #=> true
-
#
-
# body = Mail::Body.new('The body')
-
# body == 'The body' #=> true
-
#
-
# body = Mail::Body.new("VGhlIGJvZHk=\n")
-
# body.encoding = 'base64'
-
# body == "The body" #=> true
-
1
def ==(other)
-
if other.class == String
-
self.decoded == other
-
else
-
super
-
end
-
end
-
-
# Accepts a string and performs a regular expression against the decoded text
-
#
-
# Examples:
-
#
-
# body = Mail::Body.new('The body')
-
# body =~ /The/ #=> 0
-
#
-
# body = Mail::Body.new("VGhlIGJvZHk=\n")
-
# body.encoding = 'base64'
-
# body =~ /The/ #=> 0
-
1
def =~(regexp)
-
self.decoded =~ regexp
-
end
-
-
# Accepts a string and performs a regular expression against the decoded text
-
#
-
# Examples:
-
#
-
# body = Mail::Body.new('The body')
-
# body.match(/The/) #=> #<MatchData "The">
-
#
-
# body = Mail::Body.new("VGhlIGJvZHk=\n")
-
# body.encoding = 'base64'
-
# body.match(/The/) #=> #<MatchData "The">
-
1
def match(regexp)
-
self.decoded.match(regexp)
-
end
-
-
# Accepts anything that responds to #to_s and checks if it's a substring of the decoded text
-
#
-
# Examples:
-
#
-
# body = Mail::Body.new('The body')
-
# body.include?('The') #=> true
-
#
-
# body = Mail::Body.new("VGhlIGJvZHk=\n")
-
# body.encoding = 'base64'
-
# body.include?('The') #=> true
-
1
def include?(other)
-
self.decoded.include?(other.to_s)
-
end
-
-
# Allows you to set the sort order of the parts, overriding the default sort order.
-
# Defaults to 'text/plain', then 'text/enriched', then 'text/html', then 'multipart/alternative'
-
# with any other content type coming after.
-
1
def set_sort_order(order)
-
@part_sort_order = order
-
end
-
-
# Allows you to sort the parts according to the default sort order, or the sort order you
-
# set with :set_sort_order.
-
#
-
# sort_parts! is also called from :encode, so there is no need for you to call this explicitly
-
1
def sort_parts!
-
@parts.each do |p|
-
p.body.set_sort_order(@part_sort_order)
-
p.body.sort_parts!
-
end
-
@parts.sort!(@part_sort_order)
-
end
-
-
# Returns the raw source that the body was initialized with, without
-
# any tampering
-
1
def raw_source
-
@raw_source
-
end
-
-
1
def negotiate_best_encoding(message_encoding, allowed_encodings = nil)
-
Mail::Encodings::TransferEncoding.negotiate(message_encoding, encoding, raw_source, allowed_encodings)
-
end
-
-
# Returns a body encoded using transfer_encoding. Multipart always uses an
-
# identiy encoding (i.e. no encoding).
-
# Calling this directly is not a good idea, but supported for compatibility
-
# TODO: Validate that preamble and epilogue are valid for requested encoding
-
1
def encoded(transfer_encoding = nil)
-
if multipart?
-
self.sort_parts!
-
encoded_parts = parts.map { |p| p.encoded }
-
([preamble] + encoded_parts).join(crlf_boundary) + end_boundary + epilogue.to_s
-
else
-
dec = Mail::Encodings.get_encoding(encoding)
-
enc =
-
if Utilities.blank?(transfer_encoding)
-
dec
-
else
-
negotiate_best_encoding(transfer_encoding)
-
end
-
-
if dec.nil?
-
# Cannot decode, so skip normalization
-
raw_source
-
else
-
# Decode then encode to normalize and allow transforming
-
# from base64 to Q-P and vice versa
-
decoded = dec.decode(raw_source)
-
if defined?(Encoding) && charset && charset != "US-ASCII"
-
decoded = decoded.encode(charset)
-
decoded.force_encoding('BINARY') unless Encoding.find(charset).ascii_compatible?
-
end
-
enc.encode(decoded)
-
end
-
end
-
end
-
-
1
def decoded
-
if !Encodings.defined?(encoding)
-
raise UnknownEncodingType, "Don't know how to decode #{encoding}, please call #encoded and decode it yourself."
-
else
-
Encodings.get_encoding(encoding).decode(raw_source)
-
end
-
end
-
-
1
def to_s
-
decoded
-
end
-
-
1
def charset
-
@charset
-
end
-
-
1
def charset=( val )
-
@charset = val
-
end
-
-
1
def encoding(val = nil)
-
if val
-
self.encoding = val
-
else
-
@encoding
-
end
-
end
-
-
1
def encoding=( val )
-
@encoding =
-
if val == "text" || Utilities.blank?(val)
-
default_encoding
-
else
-
val
-
end
-
end
-
-
# Returns the preamble (any text that is before the first MIME boundary)
-
1
def preamble
-
@preamble
-
end
-
-
# Sets the preamble to a string (adds text before the first MIME boundary)
-
1
def preamble=( val )
-
@preamble = val
-
end
-
-
# Returns the epilogue (any text that is after the last MIME boundary)
-
1
def epilogue
-
@epilogue
-
end
-
-
# Sets the epilogue to a string (adds text after the last MIME boundary)
-
1
def epilogue=( val )
-
@epilogue = val
-
end
-
-
# Returns true if there are parts defined in the body
-
1
def multipart?
-
true unless parts.empty?
-
end
-
-
# Returns the boundary used by the body
-
1
def boundary
-
@boundary
-
end
-
-
# Allows you to change the boundary of this Body object
-
1
def boundary=( val )
-
@boundary = val
-
end
-
-
1
def parts
-
@parts
-
end
-
-
1
def <<( val )
-
if @parts
-
@parts << val
-
else
-
@parts = Mail::PartsList.new[val]
-
end
-
end
-
-
1
def split!(boundary)
-
self.boundary = boundary
-
parts = extract_parts
-
-
# Make the preamble equal to the preamble (if any)
-
self.preamble = parts[0].to_s.strip
-
# Make the epilogue equal to the epilogue (if any)
-
self.epilogue = parts[-1].to_s.strip
-
parts[1...-1].to_a.each { |part| @parts << Mail::Part.new(part) }
-
self
-
end
-
-
1
def ascii_only?
-
unless defined? @ascii_only
-
@ascii_only = raw_source.ascii_only?
-
end
-
@ascii_only
-
end
-
-
1
def empty?
-
!!raw_source.to_s.empty?
-
end
-
-
1
def default_encoding
-
ascii_only? ? '7bit' : '8bit'
-
end
-
-
1
private
-
-
# split parts by boundary, ignore first part if empty, append final part when closing boundary was missing
-
1
def extract_parts
-
parts_regex = /
-
(?: # non-capturing group
-
\A | # start of string OR
-
\r\n # line break
-
)
-
(
-
--#{Regexp.escape(boundary || "")} # boundary delimiter
-
(?:--)? # with non-capturing optional closing
-
)
-
(?=\s*$) # lookahead matching zero or more spaces followed by line-ending
-
/x
-
parts = raw_source.split(parts_regex).each_slice(2).to_a
-
parts.each_with_index { |(part, _), index| parts.delete_at(index) if index > 0 && Utilities.blank?(part) }
-
-
if parts.size > 1
-
final_separator = parts[-2][1]
-
parts << [""] if final_separator != "--#{boundary}--"
-
end
-
parts.map(&:first)
-
end
-
-
1
def crlf_boundary
-
"\r\n--#{boundary}\r\n"
-
end
-
-
1
def end_boundary
-
"\r\n--#{boundary}--\r\n"
-
end
-
-
1
def set_charset
-
@charset = ascii_only? ? 'US-ASCII' : nil
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
module Mail
-
1
module CheckDeliveryParams #:nodoc:
-
1
class << self
-
1
def check(mail)
-
[ check_from(mail.smtp_envelope_from),
-
check_to(mail.smtp_envelope_to),
-
check_message(mail) ]
-
end
-
-
1
def check_from(addr)
-
if Utilities.blank?(addr)
-
raise ArgumentError, "SMTP From address may not be blank: #{addr.inspect}"
-
end
-
-
check_addr 'From', addr
-
end
-
-
1
def check_to(addrs)
-
if Utilities.blank?(addrs)
-
raise ArgumentError, "SMTP To address may not be blank: #{addrs.inspect}"
-
end
-
-
Array(addrs).map do |addr|
-
check_addr 'To', addr
-
end
-
end
-
-
1
def check_addr(addr_name, addr)
-
validate_smtp_addr addr do |error_message|
-
raise ArgumentError, "SMTP #{addr_name} address #{error_message}: #{addr.inspect}"
-
end
-
end
-
-
1
def validate_smtp_addr(addr)
-
if addr
-
if addr.bytesize > 2048
-
yield 'may not exceed 2kB'
-
end
-
-
if /[\r\n]/ =~ addr
-
yield 'may not contain CR or LF line breaks'
-
end
-
end
-
-
addr
-
end
-
-
1
def check_message(message)
-
message = message.encoded if message.respond_to?(:encoded)
-
-
if Utilities.blank?(message)
-
raise ArgumentError, 'An encoded message is required to send an email'
-
end
-
-
message
-
end
-
end
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
#
-
# Thanks to Nicolas Fouch�� for this wrapper
-
#
-
1
require 'singleton'
-
-
1
module Mail
-
-
# The Configuration class is a Singleton used to hold the default
-
# configuration for all Mail objects.
-
#
-
# Each new mail object gets a copy of these values at initialization
-
# which can be overwritten on a per mail object basis.
-
1
class Configuration
-
1
include Singleton
-
-
1
def initialize
-
@delivery_method = nil
-
@retriever_method = nil
-
super
-
end
-
-
1
def delivery_method(method = nil, settings = {})
-
return @delivery_method if @delivery_method && method.nil?
-
@delivery_method = lookup_delivery_method(method).new(settings)
-
end
-
-
1
def lookup_delivery_method(method)
-
case method.is_a?(String) ? method.to_sym : method
-
when nil
-
Mail::SMTP
-
when :smtp
-
Mail::SMTP
-
when :sendmail
-
Mail::Sendmail
-
when :exim
-
Mail::Exim
-
when :file
-
Mail::FileDelivery
-
when :smtp_connection
-
Mail::SMTPConnection
-
when :test
-
Mail::TestMailer
-
when :logger
-
Mail::LoggerDelivery
-
else
-
method
-
end
-
end
-
-
1
def retriever_method(method = nil, settings = {})
-
return @retriever_method if @retriever_method && method.nil?
-
@retriever_method = lookup_retriever_method(method).new(settings)
-
end
-
-
1
def lookup_retriever_method(method)
-
case method
-
when nil
-
Mail::POP3
-
when :pop3
-
Mail::POP3
-
when :imap
-
Mail::IMAP
-
when :test
-
Mail::TestRetriever
-
else
-
method
-
end
-
end
-
-
1
def param_encode_language(value = nil)
-
value ? @encode_language = value : @encode_language ||= 'en'
-
end
-
-
end
-
-
end
-
# encoding: us-ascii
-
# frozen_string_literal: true
-
1
module Mail
-
1
module Constants
-
1
white_space = %Q|\x9\x20|
-
1
text = %Q|\x1-\x8\xB\xC\xE-\x7f|
-
1
field_name = %Q|\x21-\x39\x3b-\x7e|
-
1
qp_safe = %Q|\x20-\x3c\x3e-\x7e|
-
-
1
aspecial = %Q|()<>[]:;@\\,."| # RFC5322
-
1
tspecial = %Q|()<>@,;:\\"/[]?=| # RFC2045
-
1
sp = %Q| |
-
1
control = %Q|\x00-\x1f\x7f-\xff|
-
-
1
if control.respond_to?(:force_encoding)
-
1
control = control.dup.force_encoding(Encoding::BINARY)
-
end
-
-
1
CRLF = /\r?\n/
-
1
WSP = /[#{white_space}]/
-
1
FWS = /#{CRLF}#{WSP}*/
-
1
TEXT = /[#{text}]/ # + obs-text
-
1
FIELD_NAME = /[#{field_name}]+/
-
1
FIELD_PREFIX = /\A(#{FIELD_NAME})/
-
1
FIELD_BODY = /.+/m
-
1
FIELD_LINE = /^[#{field_name}]+:\s*.+$/
-
1
FIELD_SPLIT = /^(#{FIELD_NAME})\s*:\s*(#{FIELD_BODY})?$/
-
1
HEADER_LINE = /^([#{field_name}]+:\s*.+)$/
-
1
HEADER_SPLIT = /#{CRLF}(?!#{WSP})/
-
-
1
QP_UNSAFE = /[^#{qp_safe}]/
-
1
QP_SAFE = /[#{qp_safe}]/
-
1
CONTROL_CHAR = /[#{control}]/n
-
1
ATOM_UNSAFE = /[#{Regexp.quote aspecial}#{control}#{sp}]/n
-
1
PHRASE_UNSAFE = /[#{Regexp.quote aspecial}#{control}]/n
-
1
TOKEN_UNSAFE = /[#{Regexp.quote tspecial}#{control}#{sp}]/n
-
1
ENCODED_VALUE = /\=\?([^?]+)\?([QB])\?[^?]*?\?\=/mi
-
1
FULL_ENCODED_VALUE = /(\=\?[^?]+\?[QB]\?[^?]*?\?\=)/mi
-
-
1
EMPTY = ''
-
1
SPACE = ' '
-
1
UNDERSCORE = '_'
-
1
HYPHEN = '-'
-
1
COLON = ':'
-
1
ASTERISK = '*'
-
1
CR = "\r"
-
1
LF = "\n"
-
1
CR_ENCODED = "=0D"
-
1
LF_ENCODED = "=0A"
-
1
CAPITAL_M = 'M'
-
1
EQUAL_LF = "=\n"
-
1
NULL_SENDER = '<>'
-
-
1
Q_VALUES = ['Q','q']
-
1
B_VALUES = ['B','b']
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
-
# This is a backport of r30294 from ruby trunk because of a bug in net/smtp.
-
# http://svn.ruby-lang.org/cgi-bin/viewvc.cgi?view=rev&revision=30294
-
#
-
# Fixed in Ruby 1.9.3 - tlsconnect also does not exist in some early versions of ruby
-
1
if RUBY_VERSION < '1.9.3'
-
module Net
-
class SMTP
-
begin
-
alias_method :original_tlsconnect, :tlsconnect
-
-
def tlsconnect(s)
-
verified = false
-
begin
-
original_tlsconnect(s).tap { verified = true }
-
ensure
-
unless verified
-
s.close rescue nil
-
end
-
end
-
end
-
rescue NameError
-
end
-
end
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
1
class String #:nodoc:
-
-
1
unless method_defined?(:ascii_only?)
-
# Backport from Ruby 1.9 checks for non-us-ascii characters.
-
def ascii_only?
-
self !~ MATCH_NON_US_ASCII
-
end
-
-
MATCH_NON_US_ASCII = /[^\x00-\x7f]/
-
end
-
-
1
unless method_defined?(:bytesize)
-
alias :bytesize :length
-
end
-
end
-
# frozen_string_literal: true
-
1
module Mail
-
1
register_autoload :Address, 'mail/elements/address'
-
1
register_autoload :AddressList, 'mail/elements/address_list'
-
1
register_autoload :ContentDispositionElement, 'mail/elements/content_disposition_element'
-
1
register_autoload :ContentLocationElement, 'mail/elements/content_location_element'
-
1
register_autoload :ContentTransferEncodingElement, 'mail/elements/content_transfer_encoding_element'
-
1
register_autoload :ContentTypeElement, 'mail/elements/content_type_element'
-
1
register_autoload :DateTimeElement, 'mail/elements/date_time_element'
-
1
register_autoload :EnvelopeFromElement, 'mail/elements/envelope_from_element'
-
1
register_autoload :MessageIdsElement, 'mail/elements/message_ids_element'
-
1
register_autoload :MimeVersionElement, 'mail/elements/mime_version_element'
-
1
register_autoload :PhraseList, 'mail/elements/phrase_list'
-
1
register_autoload :ReceivedElement, 'mail/elements/received_element'
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
-
1
module Mail
-
# Raised when attempting to decode an unknown encoding type
-
1
class UnknownEncodingType < StandardError #:nodoc:
-
end
-
-
1
module Encodings
-
1
include Mail::Constants
-
1
extend Mail::Utilities
-
-
1
@transfer_encodings = {}
-
-
# Register transfer encoding
-
#
-
# Example
-
#
-
# Encodings.register "base64", Mail::Encodings::Base64
-
1
def Encodings.register(name, cls)
-
8
@transfer_encodings[get_name(name)] = cls
-
end
-
-
# Is the encoding we want defined?
-
#
-
# Example:
-
#
-
# Encodings.defined?(:base64) #=> true
-
1
def Encodings.defined?(name)
-
@transfer_encodings.include? get_name(name)
-
end
-
-
# Gets a defined encoding type, QuotedPrintable or Base64 for now.
-
#
-
# Each encoding needs to be defined as a Mail::Encodings::ClassName for
-
# this to work, allows us to add other encodings in the future.
-
#
-
# Example:
-
#
-
# Encodings.get_encoding(:base64) #=> Mail::Encodings::Base64
-
1
def Encodings.get_encoding(name)
-
@transfer_encodings[get_name(name)]
-
end
-
-
1
def Encodings.get_all
-
@transfer_encodings.values
-
end
-
-
1
def Encodings.get_name(name)
-
8
underscoreize(name).downcase
-
end
-
-
1
def Encodings.transcode_charset(str, from_charset, to_charset = 'UTF-8')
-
if from_charset
-
RubyVer.transcode_charset str, from_charset, to_charset
-
else
-
str
-
end
-
end
-
-
# Encodes a parameter value using URI Escaping, note the language field 'en' can
-
# be set using Mail::Configuration, like so:
-
#
-
# Mail.defaults do
-
# param_encode_language 'jp'
-
# end
-
#
-
# The character set used for encoding will either be the value of $KCODE for
-
# Ruby < 1.9 or the encoding on the string passed in.
-
#
-
# Example:
-
#
-
# Mail::Encodings.param_encode("This is fun") #=> "us-ascii'en'This%20is%20fun"
-
1
def Encodings.param_encode(str)
-
case
-
when str.ascii_only? && str =~ TOKEN_UNSAFE
-
%Q{"#{str}"}
-
when str.ascii_only?
-
str
-
else
-
RubyVer.param_encode(str)
-
end
-
end
-
-
# Decodes a parameter value using URI Escaping.
-
#
-
# Example:
-
#
-
# Mail::Encodings.param_decode("This%20is%20fun", 'us-ascii') #=> "This is fun"
-
#
-
# str = Mail::Encodings.param_decode("This%20is%20fun", 'iso-8559-1')
-
# str.encoding #=> 'ISO-8859-1' ## Only on Ruby 1.9
-
# str #=> "This is fun"
-
1
def Encodings.param_decode(str, encoding)
-
RubyVer.param_decode(str, encoding)
-
end
-
-
# Decodes or encodes a string as needed for either Base64 or QP encoding types in
-
# the =?<encoding>?[QB]?<string>?=" format.
-
#
-
# The output type needs to be :decode to decode the input string or :encode to
-
# encode the input string. The character set used for encoding will either be
-
# the value of $KCODE for Ruby < 1.9 or the encoding on the string passed in.
-
#
-
# On encoding, will only send out Base64 encoded strings.
-
1
def Encodings.decode_encode(str, output_type)
-
case
-
when output_type == :decode
-
Encodings.value_decode(str)
-
else
-
if str.ascii_only?
-
str
-
else
-
Encodings.b_value_encode(str, find_encoding(str))
-
end
-
end
-
end
-
-
# Decodes a given string as Base64 or Quoted Printable, depending on what
-
# type it is.
-
#
-
# String has to be of the format =?<encoding>?[QB]?<string>?=
-
1
def Encodings.value_decode(str)
-
# Optimization: If there's no encoded-words in the string, just return it
-
return str unless str =~ ENCODED_VALUE
-
-
lines = collapse_adjacent_encodings(str)
-
-
# Split on white-space boundaries with capture, so we capture the white-space as well
-
lines.each do |line|
-
line.gsub!(ENCODED_VALUE) do |string|
-
case $2
-
when *B_VALUES then b_value_decode(string)
-
when *Q_VALUES then q_value_decode(string)
-
end
-
end
-
end.join("")
-
end
-
-
# Takes an encoded string of the format =?<encoding>?[QB]?<string>?=
-
1
def Encodings.unquote_and_convert_to(str, to_encoding)
-
output = value_decode( str ).to_s # output is already converted to UTF-8
-
-
if 'utf8' == to_encoding.to_s.downcase.gsub("-", "")
-
output
-
elsif to_encoding
-
begin
-
if RUBY_VERSION >= '1.9'
-
output.encode(to_encoding)
-
else
-
require 'iconv'
-
Iconv.iconv(to_encoding, 'UTF-8', output).first
-
end
-
rescue Iconv::IllegalSequence, Iconv::InvalidEncoding, Errno::EINVAL
-
# the 'from' parameter specifies a charset other than what the text
-
# actually is...not much we can do in this case but just return the
-
# unconverted text.
-
#
-
# Ditto if either parameter represents an unknown charset, like
-
# X-UNKNOWN.
-
output
-
end
-
else
-
output
-
end
-
end
-
-
1
def Encodings.address_encode(address, charset = 'utf-8')
-
if address.is_a?(Array)
-
address.compact.map { |a| Encodings.address_encode(a, charset) }.join(", ")
-
elsif address
-
encode_non_usascii(address, charset)
-
end
-
end
-
-
1
def Encodings.encode_non_usascii(address, charset)
-
return address if address.ascii_only? or charset.nil?
-
-
# With KCODE=u we can't use regexps on other encodings. Go ASCII.
-
with_ascii_kcode do
-
# Encode all strings embedded inside of quotes
-
address = address.gsub(/("[^"]*[^\/]")/) { |s| Encodings.b_value_encode(unquote(s), charset) }
-
-
# Then loop through all remaining items and encode as needed
-
tokens = address.split(/\s/)
-
-
map_with_index(tokens) do |word, i|
-
if word.ascii_only?
-
word
-
else
-
previous_non_ascii = i>0 && tokens[i-1] && !tokens[i-1].ascii_only?
-
if previous_non_ascii #why are we adding an extra space here?
-
word = " #{word}"
-
end
-
Encodings.b_value_encode(word, charset)
-
end
-
end.join(' ')
-
end
-
end
-
-
1
if RUBY_VERSION < '1.9'
-
# With KCODE=u we can't use regexps on other encodings. Go ASCII.
-
def Encodings.with_ascii_kcode #:nodoc:
-
if $KCODE
-
$KCODE, original_kcode = '', $KCODE
-
end
-
yield
-
ensure
-
$KCODE = original_kcode if original_kcode
-
end
-
else
-
1
def Encodings.with_ascii_kcode #:nodoc:
-
yield
-
end
-
end
-
-
# Encode a string with Base64 Encoding and returns it ready to be inserted
-
# as a value for a field, that is, in the =?<charset>?B?<string>?= format
-
#
-
# Example:
-
#
-
# Encodings.b_value_encode('This is ��� string', 'UTF-8')
-
# #=> "=?UTF-8?B?VGhpcyBpcyDjgYIgc3RyaW5n?="
-
1
def Encodings.b_value_encode(string, encoding = nil)
-
if string.to_s.ascii_only?
-
string
-
else
-
Encodings.each_base64_chunk_byterange(string, 60).map do |chunk|
-
str, encoding = RubyVer.b_value_encode(chunk, encoding)
-
"=?#{encoding}?B?#{str.chomp}?="
-
end.join(" ")
-
end
-
end
-
-
# Encode a string with Quoted-Printable Encoding and returns it ready to be inserted
-
# as a value for a field, that is, in the =?<charset>?Q?<string>?= format
-
#
-
# Example:
-
#
-
# Encodings.q_value_encode('This is ��� string', 'UTF-8')
-
# #=> "=?UTF-8?Q?This_is_=E3=81=82_string?="
-
1
def Encodings.q_value_encode(encoded_str, encoding = nil)
-
return encoded_str if encoded_str.to_s.ascii_only?
-
string, encoding = RubyVer.q_value_encode(encoded_str, encoding)
-
string.gsub!("=\r\n", '') # We already have limited the string to the length we want
-
map_lines(string) do |str|
-
"=?#{encoding}?Q?#{str.chomp.gsub(/ /, '_')}?="
-
end.join(" ")
-
end
-
-
1
private
-
-
# Decodes a Base64 string from the "=?UTF-8?B?VGhpcyBpcyDjgYIgc3RyaW5n?=" format
-
#
-
# Example:
-
#
-
# Encodings.b_value_decode("=?UTF-8?B?VGhpcyBpcyDjgYIgc3RyaW5n?=")
-
# #=> 'This is ��� string'
-
1
def Encodings.b_value_decode(str)
-
RubyVer.b_value_decode(str)
-
end
-
-
# Decodes a Quoted-Printable string from the "=?UTF-8?Q?This_is_=E3=81=82_string?=" format
-
#
-
# Example:
-
#
-
# Encodings.q_value_decode("=?UTF-8?Q?This_is_=E3=81=82_string?=")
-
# #=> 'This is ��� string'
-
1
def Encodings.q_value_decode(str)
-
RubyVer.q_value_decode(str)
-
end
-
-
1
def Encodings.find_encoding(str)
-
RUBY_VERSION >= '1.9' ? str.encoding : $KCODE
-
end
-
-
# Gets the encoding type (Q or B) from the string.
-
1
def Encodings.value_encoding_from_string(str)
-
str[ENCODED_VALUE, 1]
-
end
-
-
# Split header line into proper encoded and unencoded parts.
-
#
-
# String has to be of the format =?<encoding>?[QB]?<string>?=
-
#
-
# Omit unencoded space after an encoded-word.
-
1
def Encodings.collapse_adjacent_encodings(str)
-
results = []
-
last_encoded = nil # Track whether to preserve or drop whitespace
-
-
lines = str.split(FULL_ENCODED_VALUE)
-
lines.each_slice(2) do |unencoded, encoded|
-
if last_encoded = encoded
-
if !Utilities.blank?(unencoded) || (!last_encoded && unencoded != EMPTY)
-
results << unencoded
-
end
-
-
results << encoded
-
else
-
results << unencoded
-
end
-
end
-
-
results
-
end
-
-
# Partition the string into bounded-size chunks without splitting
-
# multibyte characters.
-
1
def Encodings.each_base64_chunk_byterange(str, max_bytesize_per_base64_chunk, &block)
-
raise "size per chunk must be multiple of 4" if (max_bytesize_per_base64_chunk % 4).nonzero?
-
-
if block_given?
-
max_bytesize = ((3 * max_bytesize_per_base64_chunk) / 4.0).floor
-
each_chunk_byterange(str, max_bytesize, &block)
-
else
-
enum_for :each_base64_chunk_byterange, str, max_bytesize_per_base64_chunk
-
end
-
end
-
-
# Partition the string into bounded-size chunks without splitting
-
# multibyte characters.
-
1
def Encodings.each_chunk_byterange(str, max_bytesize_per_chunk)
-
return enum_for(:each_chunk_byterange, str, max_bytesize_per_chunk) unless block_given?
-
-
offset = 0
-
chunksize = 0
-
-
str.each_char do |chr|
-
charsize = chr.bytesize
-
-
if chunksize + charsize > max_bytesize_per_chunk
-
yield RubyVer.string_byteslice(str, offset, chunksize)
-
offset += chunksize
-
chunksize = charsize
-
else
-
chunksize += charsize
-
end
-
end
-
-
yield RubyVer.string_byteslice(str, offset, chunksize)
-
end
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
1
require 'mail/encodings/8bit'
-
-
1
module Mail
-
1
module Encodings
-
# 7bit and 8bit are equivalent. 7bit encoding is for text only.
-
1
class SevenBit < EightBit
-
1
NAME = '7bit'
-
1
PRIORITY = 1
-
1
Encodings.register(NAME, self)
-
-
1
def self.decode(str)
-
::Mail::Utilities.binary_unsafe_to_lf str
-
end
-
-
1
def self.encode(str)
-
::Mail::Utilities.binary_unsafe_to_crlf str
-
end
-
end
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
1
require 'mail/encodings/binary'
-
-
1
module Mail
-
1
module Encodings
-
1
class EightBit < Binary
-
1
NAME = '8bit'
-
1
PRIORITY = 4
-
1
Encodings.register(NAME, self)
-
-
# Per RFC 2821 4.5.3.1, SMTP lines may not be longer than 1000 octets including the <CRLF>.
-
1
def self.compatible_input?(str)
-
!str.lines.find { |line| line.bytesize > 998 }
-
end
-
end
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
1
require 'mail/encodings/7bit'
-
-
1
module Mail
-
1
module Encodings
-
# Base64 encoding handles binary content at the cost of 4 output bytes
-
# per input byte.
-
1
class Base64 < SevenBit
-
1
NAME = 'base64'
-
1
PRIORITY = 3
-
1
Encodings.register(NAME, self)
-
-
1
def self.can_encode?(enc)
-
true
-
end
-
-
1
def self.decode(str)
-
RubyVer.decode_base64(str)
-
end
-
-
1
def self.encode(str)
-
::Mail::Utilities.binary_unsafe_to_crlf(RubyVer.encode_base64(str))
-
end
-
-
# 3 bytes in -> 4 bytes out
-
1
def self.cost(str)
-
4.0 / 3
-
end
-
-
# Ruby Base64 inserts newlines automatically, so it doesn't exceed
-
# SMTP line length limits.
-
1
def self.compatible_input?(str)
-
true
-
end
-
end
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
1
require 'mail/encodings/identity'
-
-
1
module Mail
-
1
module Encodings
-
1
class Binary < Identity
-
1
NAME = 'binary'
-
1
PRIORITY = 5
-
1
Encodings.register(NAME, self)
-
end
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
1
require 'mail/encodings/transfer_encoding'
-
-
1
module Mail
-
1
module Encodings
-
# Identity encodings do no encoding/decoding and have a fixed cost:
-
# 1 byte in -> 1 byte out.
-
1
class Identity < TransferEncoding #:nodoc:
-
1
def self.decode(str)
-
str
-
end
-
-
1
def self.encode(str)
-
str
-
end
-
-
# 1 output byte per input byte.
-
1
def self.cost(str)
-
1.0
-
end
-
end
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
1
require 'mail/encodings/7bit'
-
-
1
module Mail
-
1
module Encodings
-
1
class QuotedPrintable < SevenBit
-
1
NAME='quoted-printable'
-
-
1
PRIORITY = 2
-
-
1
def self.can_encode?(enc)
-
EightBit.can_encode? enc
-
end
-
-
# Decode the string from Quoted-Printable. Cope with hard line breaks
-
# that were incorrectly encoded as hex instead of literal CRLF.
-
1
def self.decode(str)
-
str.gsub(/(?:=0D=0A|=0D|=0A)\r\n/, "\r\n").unpack("M*").first
-
end
-
-
1
def self.encode(str)
-
[str].pack("M")
-
end
-
-
1
def self.cost(str)
-
# These bytes probably do not need encoding
-
c = str.count("\x9\xA\xD\x20-\x3C\x3E-\x7E")
-
# Everything else turns into =XX where XX is a
-
# two digit hex number (taking 3 bytes)
-
total = (str.bytesize - c)*3 + c
-
total.to_f/str.bytesize
-
end
-
-
# QP inserts newlines automatically and cannot violate the SMTP spec.
-
1
def self.compatible_input?(str)
-
true
-
end
-
-
1
private
-
-
1
Encodings.register(NAME, self)
-
end
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
1
module Mail
-
1
module Encodings
-
1
class TransferEncoding
-
1
NAME = ''
-
-
1
PRIORITY = -1
-
-
# And encoding's superclass can always transport it since the
-
# class hierarchy is arranged e.g. Base64 < 7bit < 8bit < Binary.
-
1
def self.can_transport?(enc)
-
enc && enc <= self
-
end
-
-
# Override in subclasses to indicate that they can encode text
-
# that couldn't be directly transported, e.g. Base64 has 7bit output,
-
# but it can encode binary.
-
1
def self.can_encode?(enc)
-
can_transport? enc
-
end
-
-
1
def self.cost(str)
-
raise "Unimplemented"
-
end
-
-
1
def self.compatible_input?(str)
-
true
-
end
-
-
1
def self.to_s
-
self::NAME
-
end
-
-
1
def self.negotiate(message_encoding, source_encoding, str, allowed_encodings = nil)
-
message_encoding = Encodings.get_encoding(message_encoding) || Encodings.get_encoding('8bit')
-
source_encoding = Encodings.get_encoding(source_encoding)
-
-
if message_encoding && source_encoding && message_encoding.can_transport?(source_encoding) && source_encoding.compatible_input?(str)
-
source_encoding
-
else
-
renegotiate(message_encoding, source_encoding, str, allowed_encodings)
-
end
-
end
-
-
1
def self.renegotiate(message_encoding, source_encoding, str, allowed_encodings = nil)
-
encodings = Encodings.get_all.select do |enc|
-
(allowed_encodings.nil? || allowed_encodings.include?(enc)) &&
-
message_encoding.can_transport?(enc) &&
-
enc.can_encode?(source_encoding)
-
end
-
-
lowest_cost(str, encodings)
-
end
-
-
1
def self.lowest_cost(str, encodings)
-
best = nil
-
best_cost = nil
-
-
encodings.each do |enc|
-
# If the current choice cannot be transported safely, give priority
-
# to other choices but allow it to be used as a fallback.
-
this_cost = enc.cost(str) if enc.compatible_input?(str)
-
-
if !best_cost || (this_cost && this_cost < best_cost)
-
best_cost = this_cost
-
best = enc
-
elsif this_cost == best_cost
-
best = enc if enc::PRIORITY < best::PRIORITY
-
end
-
end
-
-
best
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
module Mail
-
1
module Encodings
-
1
class UnixToUnix < TransferEncoding
-
1
NAME = "x-uuencode"
-
-
1
def self.decode(str)
-
str.sub(/\Abegin \d+ [^\n]*\n/, '').unpack('u').first
-
end
-
-
1
def self.encode(str)
-
[str].pack("u")
-
end
-
-
1
Encodings.register(NAME, self)
-
1
Encodings.register("uuencode", self)
-
1
Encodings.register("x-uue", self)
-
end
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
#
-
# = Mail Envelope
-
#
-
# The Envelope class provides a field for the first line in an
-
# mbox file, that looks like "From mikel@test.lindsaar.net DATETIME"
-
#
-
# This envelope class reads that line, and turns it into an
-
# Envelope.from and Envelope.date for your use.
-
1
module Mail
-
1
class Envelope < StructuredField
-
-
1
def initialize(*args)
-
super(FIELD_NAME, args.last.to_s)
-
end
-
-
1
def element
-
@element ||= Mail::EnvelopeFromElement.new(value)
-
end
-
-
1
def date
-
::DateTime.parse("#{element.date_time}")
-
end
-
-
1
def from
-
element.address
-
end
-
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'mail/fields'
-
1
require 'mail/constants'
-
-
# encoding: utf-8
-
1
module Mail
-
# Provides a single class to call to create a new structured or unstructured
-
# field. Works out per RFC what field of field it is being given and returns
-
# the correct field of class back on new.
-
#
-
# ===Per RFC 2822
-
#
-
# 2.2. Header Fields
-
#
-
# Header fields are lines composed of a field name, followed by a colon
-
# (":"), followed by a field body, and terminated by CRLF. A field
-
# name MUST be composed of printable US-ASCII characters (i.e.,
-
# characters that have values between 33 and 126, inclusive), except
-
# colon. A field body may be composed of any US-ASCII characters,
-
# except for CR and LF. However, a field body may contain CRLF when
-
# used in header "folding" and "unfolding" as described in section
-
# 2.2.3. All field bodies MUST conform to the syntax described in
-
# sections 3 and 4 of this standard.
-
#
-
1
class Field
-
-
1
include Utilities
-
1
include Comparable
-
-
1
STRUCTURED_FIELDS = %w[ bcc cc content-description content-disposition
-
content-id content-location content-transfer-encoding
-
content-type date from in-reply-to keywords message-id
-
mime-version received references reply-to
-
resent-bcc resent-cc resent-date resent-from
-
resent-message-id resent-sender resent-to
-
return-path sender to ]
-
-
1
KNOWN_FIELDS = STRUCTURED_FIELDS + ['comments', 'subject']
-
-
1
FIELDS_MAP = {
-
"to" => ToField,
-
"cc" => CcField,
-
"bcc" => BccField,
-
"message-id" => MessageIdField,
-
"in-reply-to" => InReplyToField,
-
"references" => ReferencesField,
-
"subject" => SubjectField,
-
"comments" => CommentsField,
-
"keywords" => KeywordsField,
-
"date" => DateField,
-
"from" => FromField,
-
"sender" => SenderField,
-
"reply-to" => ReplyToField,
-
"resent-date" => ResentDateField,
-
"resent-from" => ResentFromField,
-
"resent-sender" => ResentSenderField,
-
"resent-to" => ResentToField,
-
"resent-cc" => ResentCcField,
-
"resent-bcc" => ResentBccField,
-
"resent-message-id" => ResentMessageIdField,
-
"return-path" => ReturnPathField,
-
"received" => ReceivedField,
-
"mime-version" => MimeVersionField,
-
"content-transfer-encoding" => ContentTransferEncodingField,
-
"content-description" => ContentDescriptionField,
-
"content-disposition" => ContentDispositionField,
-
"content-type" => ContentTypeField,
-
"content-id" => ContentIdField,
-
"content-location" => ContentLocationField,
-
}
-
-
1
FIELD_NAME_MAP = FIELDS_MAP.inject({}) do |map, (field, field_klass)|
-
29
map.update(field => field_klass::CAPITALIZED_FIELD)
-
end
-
-
# Generic Field Exception
-
1
class FieldError < StandardError
-
end
-
-
# Raised when a parsing error has occurred (ie, a StructuredField has tried
-
# to parse a field that is invalid or improperly written)
-
1
class ParseError < FieldError #:nodoc:
-
1
attr_accessor :element, :value, :reason
-
-
1
def initialize(element, value, reason)
-
@element = element
-
@value = to_utf8(value)
-
@reason = to_utf8(reason)
-
super("#{@element} can not parse |#{@value}|: #{@reason}")
-
end
-
-
1
private
-
1
def to_utf8(text)
-
if text.respond_to?(:force_encoding)
-
text.dup.force_encoding(Encoding::UTF_8)
-
else
-
text
-
end
-
end
-
end
-
-
1
class NilParseError < ParseError #:nodoc:
-
1
def initialize(element)
-
super element, nil, 'nil is invalid'
-
end
-
end
-
-
1
class IncompleteParseError < ParseError #:nodoc:
-
1
def initialize(element, original_text, unparsed_index)
-
parsed_text = to_utf8(original_text[0...unparsed_index])
-
super element, original_text, "Only able to parse up to #{parsed_text.inspect}"
-
end
-
end
-
-
# Raised when attempting to set a structured field's contents to an invalid syntax
-
1
class SyntaxError < FieldError #:nodoc:
-
end
-
-
1
class << self
-
# Parse a field from a raw header line:
-
#
-
# Mail::Field.parse("field-name: field data")
-
# # => #<Mail::Field ���>
-
1
def parse(field, charset = nil)
-
name, value = split(field)
-
if name && value
-
new name, value, charset
-
end
-
end
-
-
1
def split(raw_field) #:nodoc:
-
if raw_field.index(Constants::COLON)
-
name, value = raw_field.split(Constants::COLON, 2)
-
name.rstrip!
-
if name =~ /\A#{Constants::FIELD_NAME}\z/
-
[ name.rstrip, value.strip ]
-
else
-
Kernel.warn "WARNING: Ignoring unparsable header #{raw_field.inspect}: invalid header name syntax: #{name.inspect}"
-
nil
-
end
-
else
-
raw_field.strip
-
end
-
rescue => error
-
warn "WARNING: Ignoring unparsable header #{raw_field.inspect}: #{error.class}: #{error.message}"
-
nil
-
end
-
end
-
-
1
attr_reader :unparsed_value
-
-
# Create a field by name and optional value:
-
#
-
# Mail::Field.new("field-name", "value")
-
# # => #<Mail::Field ���>
-
#
-
# Values that aren't strings or arrays are coerced to Strings with `#to_s`.
-
#
-
# Mail::Field.new("field-name", 1234)
-
# # => #<Mail::Field ���>
-
#
-
# Mail::Field.new('content-type', ['text', 'plain', {:charset => 'UTF-8'}])
-
# # => #<Mail::Field ���>
-
1
def initialize(name, value = nil, charset = 'utf-8')
-
case
-
when name.index(COLON)
-
Kernel.warn 'Passing an unparsed header field to Mail::Field.new is deprecated and will be removed in Mail 2.8.0. Use Mail::Field.parse instead.'
-
@name, @unparsed_value = self.class.split(name)
-
@charset = Utilities.blank?(value) ? charset : value
-
when Utilities.blank?(value)
-
@name = name
-
@unparsed_value = nil
-
@charset = charset
-
else
-
@name = name
-
@unparsed_value = value
-
@charset = charset
-
end
-
@name = FIELD_NAME_MAP[@name.to_s.downcase] || @name
-
end
-
-
1
def field=(value)
-
@field = value
-
end
-
-
1
def field
-
@field ||= create_field(@name, @unparsed_value, @charset)
-
end
-
-
1
def name
-
@name
-
end
-
-
1
def value
-
field.value
-
end
-
-
1
def value=(val)
-
@field = create_field(name, val, @charset)
-
end
-
-
1
def to_s
-
field.to_s
-
end
-
-
1
def inspect
-
"#<#{self.class.name} 0x#{(object_id * 2).to_s(16)} #{instance_variables.map do |ivar|
-
"#{ivar}=#{instance_variable_get(ivar).inspect}"
-
end.join(" ")}>"
-
end
-
-
1
def update(name, value)
-
@field = create_field(name, value, @charset)
-
end
-
-
1
def same( other )
-
return false unless other.kind_of?(self.class)
-
match_to_s(other.name, self.name)
-
end
-
-
1
def ==( other )
-
return false unless other.kind_of?(self.class)
-
match_to_s(other.name, self.name) && match_to_s(other.value, self.value)
-
end
-
-
1
def responsible_for?( val )
-
name.to_s.casecmp(val.to_s) == 0
-
end
-
-
1
def <=>( other )
-
self.field_order_id <=> other.field_order_id
-
end
-
-
1
def field_order_id
-
@field_order_id ||= (FIELD_ORDER_LOOKUP[self.name.to_s.downcase] || 100)
-
end
-
-
1
def method_missing(name, *args, &block)
-
field.send(name, *args, &block)
-
end
-
-
1
if RUBY_VERSION >= '1.9.2'
-
1
def respond_to_missing?(method_name, include_private)
-
field.respond_to?(method_name, include_private) || super
-
end
-
else
-
def respond_to?(method_name, include_private = false)
-
field.respond_to?(method_name, include_private) || super
-
end
-
end
-
-
1
FIELD_ORDER = %w[ return-path received
-
resent-date resent-from resent-sender resent-to
-
resent-cc resent-bcc resent-message-id
-
date from sender reply-to to cc bcc
-
message-id in-reply-to references
-
subject comments keywords
-
mime-version content-type content-transfer-encoding
-
content-location content-disposition content-description ]
-
-
1
FIELD_ORDER_LOOKUP = Hash[FIELD_ORDER.each_with_index.to_a]
-
-
1
private
-
-
1
def create_field(name, value, charset)
-
new_field(name, value, charset)
-
rescue Mail::Field::ParseError => e
-
field = Mail::UnstructuredField.new(name, value)
-
field.errors << [name, value, e]
-
field
-
end
-
-
1
def new_field(name, value, charset)
-
value = unfold(value) if value.is_a?(String)
-
-
if klass = field_class_for(name)
-
klass.new(value, charset)
-
else
-
OptionalField.new(name, value, charset)
-
end
-
end
-
-
1
def field_class_for(name)
-
FIELDS_MAP[name.to_s.downcase]
-
end
-
-
# 2.2.3. Long Header Fields
-
#
-
# The process of moving from this folded multiple-line representation
-
# of a header field to its single line representation is called
-
# "unfolding". Unfolding is accomplished by simply removing any CRLF
-
# that is immediately followed by WSP. Each header field should be
-
# treated in its unfolded form for further syntactic and semantic
-
# evaluation.
-
1
def unfold(string)
-
string.gsub(/#{Constants::CRLF}(#{Constants::WSP})/m, '\1')
-
end
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
1
module Mail
-
-
# Field List class provides an enhanced array that keeps a list of
-
# email fields in order. And allows you to insert new fields without
-
# having to worry about the order they will appear in.
-
1
class FieldList < Array
-
-
1
include Enumerable
-
-
# Insert the field in sorted order.
-
#
-
# Heavily based on bisect.insort from Python, which is:
-
# Copyright (C) 2001-2013 Python Software Foundation.
-
# Licensed under <http://docs.python.org/license.html>
-
# From <http://hg.python.org/cpython/file/2.7/Lib/bisect.py>
-
1
def <<( new_field )
-
lo = 0
-
hi = size
-
-
while lo < hi
-
mid = (lo + hi).div(2)
-
if new_field < self[mid]
-
hi = mid
-
else
-
lo = mid + 1
-
end
-
end
-
-
insert(lo, new_field)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
module Mail
-
1
register_autoload :UnstructuredField, 'mail/fields/unstructured_field'
-
1
register_autoload :StructuredField, 'mail/fields/structured_field'
-
1
register_autoload :OptionalField, 'mail/fields/optional_field'
-
-
1
register_autoload :BccField, 'mail/fields/bcc_field'
-
1
register_autoload :CcField, 'mail/fields/cc_field'
-
1
register_autoload :CommentsField, 'mail/fields/comments_field'
-
1
register_autoload :ContentDescriptionField, 'mail/fields/content_description_field'
-
1
register_autoload :ContentDispositionField, 'mail/fields/content_disposition_field'
-
1
register_autoload :ContentIdField, 'mail/fields/content_id_field'
-
1
register_autoload :ContentLocationField, 'mail/fields/content_location_field'
-
1
register_autoload :ContentTransferEncodingField, 'mail/fields/content_transfer_encoding_field'
-
1
register_autoload :ContentTypeField, 'mail/fields/content_type_field'
-
1
register_autoload :DateField, 'mail/fields/date_field'
-
1
register_autoload :FromField, 'mail/fields/from_field'
-
1
register_autoload :InReplyToField, 'mail/fields/in_reply_to_field'
-
1
register_autoload :KeywordsField, 'mail/fields/keywords_field'
-
1
register_autoload :MessageIdField, 'mail/fields/message_id_field'
-
1
register_autoload :MimeVersionField, 'mail/fields/mime_version_field'
-
1
register_autoload :ReceivedField, 'mail/fields/received_field'
-
1
register_autoload :ReferencesField, 'mail/fields/references_field'
-
1
register_autoload :ReplyToField, 'mail/fields/reply_to_field'
-
1
register_autoload :ResentBccField, 'mail/fields/resent_bcc_field'
-
1
register_autoload :ResentCcField, 'mail/fields/resent_cc_field'
-
1
register_autoload :ResentDateField, 'mail/fields/resent_date_field'
-
1
register_autoload :ResentFromField, 'mail/fields/resent_from_field'
-
1
register_autoload :ResentMessageIdField, 'mail/fields/resent_message_id_field'
-
1
register_autoload :ResentSenderField, 'mail/fields/resent_sender_field'
-
1
register_autoload :ResentToField, 'mail/fields/resent_to_field'
-
1
register_autoload :ReturnPathField, 'mail/fields/return_path_field'
-
1
register_autoload :SenderField, 'mail/fields/sender_field'
-
1
register_autoload :SubjectField, 'mail/fields/subject_field'
-
1
register_autoload :ToField, 'mail/fields/to_field'
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
#
-
# = Blind Carbon Copy Field
-
#
-
# The Bcc field inherits from StructuredField and handles the Bcc: header
-
# field in the email.
-
#
-
# Sending bcc to a mail message will instantiate a Mail::Field object that
-
# has a BccField as its field type. This includes all Mail::CommonAddress
-
# module instance metods.
-
#
-
# Only one Bcc field can appear in a header, though it can have multiple
-
# addresses and groups of addresses.
-
#
-
# == Examples:
-
#
-
# mail = Mail.new
-
# mail.bcc = 'Mikel Lindsaar <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail.bcc #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
# mail[:bcc] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::BccField:0x180e1c4
-
# mail['bcc'] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::BccField:0x180e1c4
-
# mail['Bcc'] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::BccField:0x180e1c4
-
#
-
# mail[:bcc].encoded #=> '' # Bcc field does not get output into an email
-
# mail[:bcc].decoded #=> 'Mikel Lindsaar <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail[:bcc].addresses #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
# mail[:bcc].formatted #=> ['Mikel Lindsaar <mikel@test.lindsaar.net>', 'ada@test.lindsaar.net']
-
#
-
1
require 'mail/fields/common/common_address'
-
-
1
module Mail
-
1
class BccField < StructuredField
-
-
1
include Mail::CommonAddress
-
-
1
FIELD_NAME = 'bcc'
-
1
CAPITALIZED_FIELD = 'Bcc'
-
-
1
def initialize(value = nil, charset = 'utf-8')
-
@charset = charset
-
super(CAPITALIZED_FIELD, value, charset)
-
self
-
end
-
-
1
def include_in_headers=(include_in_headers)
-
@include_in_headers = include_in_headers
-
end
-
-
1
def include_in_headers
-
defined?(@include_in_headers) ? @include_in_headers : self.include_in_headers = false
-
end
-
-
# Bcc field should not be :encoded by default
-
1
def encoded
-
if include_in_headers
-
do_encode(CAPITALIZED_FIELD)
-
else
-
''
-
end
-
end
-
-
1
def decoded
-
do_decode
-
end
-
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
#
-
# = Carbon Copy Field
-
#
-
# The Cc field inherits from StructuredField and handles the Cc: header
-
# field in the email.
-
#
-
# Sending cc to a mail message will instantiate a Mail::Field object that
-
# has a CcField as its field type. This includes all Mail::CommonAddress
-
# module instance metods.
-
#
-
# Only one Cc field can appear in a header, though it can have multiple
-
# addresses and groups of addresses.
-
#
-
# == Examples:
-
#
-
# mail = Mail.new
-
# mail.cc = 'Mikel Lindsaar <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail.cc #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
# mail[:cc] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::CcField:0x180e1c4
-
# mail['cc'] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::CcField:0x180e1c4
-
# mail['Cc'] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::CcField:0x180e1c4
-
#
-
# mail[:cc].encoded #=> 'Cc: Mikel Lindsaar <mikel@test.lindsaar.net>, ada@test.lindsaar.net\r\n'
-
# mail[:cc].decoded #=> 'Mikel Lindsaar <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail[:cc].addresses #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
# mail[:cc].formatted #=> ['Mikel Lindsaar <mikel@test.lindsaar.net>', 'ada@test.lindsaar.net']
-
#
-
1
require 'mail/fields/common/common_address'
-
-
1
module Mail
-
1
class CcField < StructuredField
-
-
1
include Mail::CommonAddress
-
-
1
FIELD_NAME = 'cc'
-
1
CAPITALIZED_FIELD = 'Cc'
-
-
1
def initialize(value = nil, charset = 'utf-8')
-
self.charset = charset
-
super(CAPITALIZED_FIELD, value, charset)
-
self
-
end
-
-
1
def encoded
-
do_encode(CAPITALIZED_FIELD)
-
end
-
-
1
def decoded
-
do_decode
-
end
-
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
#
-
# = Comments Field
-
#
-
# The Comments field inherits from UnstructuredField and handles the Comments:
-
# header field in the email.
-
#
-
# Sending comments to a mail message will instantiate a Mail::Field object that
-
# has a CommentsField as its field type.
-
#
-
# An email header can have as many comments fields as it wants. There is no upper
-
# limit, the comments field is also optional (that is, no comment is needed)
-
#
-
# == Examples:
-
#
-
# mail = Mail.new
-
# mail.comments = 'This is a comment'
-
# mail.comments #=> 'This is a comment'
-
# mail[:comments] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::CommentsField:0x180e1c4
-
# mail['comments'] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::CommentsField:0x180e1c4
-
# mail['comments'] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::CommentsField:0x180e1c4
-
#
-
# mail.comments = "This is another comment"
-
# mail[:comments].map { |c| c.to_s }
-
# #=> ['This is a comment', "This is another comment"]
-
#
-
1
module Mail
-
1
class CommentsField < UnstructuredField
-
-
1
FIELD_NAME = 'comments'
-
1
CAPITALIZED_FIELD = 'Comments'
-
-
1
def initialize(value = nil, charset = 'utf-8')
-
@charset = charset
-
super(CAPITALIZED_FIELD, value)
-
self.parse
-
self
-
end
-
-
end
-
end
-
# frozen_string_literal: true
-
1
module Mail
-
-
1
class AddressContainer < Array
-
-
1
def initialize(field, list = [])
-
@field = field
-
super(list)
-
end
-
-
1
def <<(address)
-
@field << address
-
end
-
-
end
-
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
1
require 'mail/fields/common/address_container'
-
-
1
module Mail
-
1
module CommonAddress # :nodoc:
-
1
def parse(val = value)
-
unless Utilities.blank?(val)
-
@address_list = AddressList.new(encode_if_needed(val))
-
else
-
nil
-
end
-
end
-
-
1
def charset
-
@charset
-
end
-
-
1
def encode_if_needed(val) #:nodoc:
-
# Need to join arrays of addresses into a single value
-
if val.kind_of?(Array)
-
val.compact.map { |a| encode_if_needed a }.join(', ')
-
-
# Pass through UTF-8; encode non-UTF-8.
-
else
-
utf8_if_needed(val) || Encodings.encode_non_usascii(val, charset)
-
end
-
end
-
-
# Allows you to iterate through each address object in the address_list
-
1
def each
-
address_list.addresses.each do |address|
-
yield(address)
-
end
-
end
-
-
# Returns the address string of all the addresses in the address list
-
1
def addresses
-
list = address_list.addresses.map { |a| a.address }
-
Mail::AddressContainer.new(self, list)
-
end
-
-
# Returns the formatted string of all the addresses in the address list
-
1
def formatted
-
list = address_list.addresses.map { |a| a.format }
-
Mail::AddressContainer.new(self, list)
-
end
-
-
# Returns the display name of all the addresses in the address list
-
1
def display_names
-
list = address_list.addresses.map { |a| a.display_name }
-
Mail::AddressContainer.new(self, list)
-
end
-
-
# Returns the actual address objects in the address list
-
1
def addrs
-
list = address_list.addresses
-
Mail::AddressContainer.new(self, list)
-
end
-
-
# Returns a hash of group name => address strings for the address list
-
1
def groups
-
address_list.addresses_grouped_by_group
-
end
-
-
# Returns the addresses that are part of groups
-
1
def group_addresses
-
decoded_group_addresses
-
end
-
-
# Returns a list of decoded group addresses
-
1
def decoded_group_addresses
-
groups.map { |k,v| v.map { |a| a.decoded } }.flatten
-
end
-
-
# Returns a list of encoded group addresses
-
1
def encoded_group_addresses
-
groups.map { |k,v| v.map { |a| a.encoded } }.flatten
-
end
-
-
# Returns the name of all the groups in a string
-
1
def group_names # :nodoc:
-
address_list.group_names
-
end
-
-
1
def default
-
addresses
-
end
-
-
1
def <<(val)
-
case
-
when val.nil?
-
raise ArgumentError, "Need to pass an address to <<"
-
when Utilities.blank?(val)
-
parse(encoded)
-
else
-
self.value = [self.value, val].reject {|a| Utilities.blank?(a) }.join(", ")
-
end
-
end
-
-
1
def value=(val)
-
super
-
parse(self.value)
-
end
-
-
1
private
-
-
1
if 'string'.respond_to?(:encoding)
-
# Pass through UTF-8 addresses
-
1
def utf8_if_needed(val)
-
if charset =~ /\AUTF-?8\z/i
-
val
-
elsif val.encoding == Encoding::UTF_8
-
val
-
elsif (utf8 = val.dup.force_encoding(Encoding::UTF_8)).valid_encoding?
-
utf8
-
end
-
end
-
else
-
def utf8_if_needed(val)
-
if charset =~ /\AUTF-?8\z/i
-
val
-
end
-
end
-
end
-
-
1
def do_encode(field_name)
-
return '' if Utilities.blank?(value)
-
address_array = address_list.addresses.reject { |a| encoded_group_addresses.include?(a.encoded) }.compact.map { |a| a.encoded }
-
address_text = address_array.join(", \r\n\s")
-
group_array = groups.map { |k,v| "#{k}: #{v.map { |a| a.encoded }.join(", \r\n\s")};" }
-
group_text = group_array.join(" \r\n\s")
-
return_array = [address_text, group_text].reject { |a| Utilities.blank?(a) }
-
"#{field_name}: #{return_array.join(", \r\n\s")}\r\n"
-
end
-
-
1
def do_decode
-
return nil if Utilities.blank?(value)
-
address_array = address_list.addresses.reject { |a| decoded_group_addresses.include?(a.decoded) }.map { |a| a.decoded }
-
address_text = address_array.join(", ")
-
group_array = groups.map { |k,v| "#{k}: #{v.map { |a| a.decoded }.join(", ")};" }
-
group_text = group_array.join(" ")
-
return_array = [address_text, group_text].reject { |a| Utilities.blank?(a) }
-
return_array.join(", ")
-
end
-
-
1
def address_list # :nodoc:
-
@address_list ||= AddressList.new(value)
-
end
-
-
1
def get_group_addresses(group_list)
-
if group_list.respond_to?(:addresses)
-
group_list.addresses.map do |address|
-
Mail::Address.new(address)
-
end
-
else
-
[]
-
end
-
end
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
1
module Mail
-
1
module CommonDate # :nodoc:
-
# Returns a date time object of the parsed date
-
1
def date_time
-
::DateTime.parse("#{element.date_string} #{element.time_string}")
-
end
-
-
1
def default
-
date_time
-
end
-
-
1
def parse(val = value)
-
unless Utilities.blank?(val)
-
@element = Mail::DateTimeElement.new(val)
-
else
-
nil
-
end
-
end
-
-
1
private
-
-
1
def do_encode(field_name)
-
"#{field_name}: #{value}\r\n"
-
end
-
-
1
def do_decode
-
"#{value}"
-
end
-
-
1
def element
-
@element ||= Mail::DateTimeElement.new(value)
-
end
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
1
module Mail
-
1
module CommonField # :nodoc:
-
1
include Mail::Constants
-
-
1
def name=(value)
-
@name = value
-
end
-
-
1
def name
-
@name ||= nil
-
end
-
-
1
def value=(value)
-
@length = nil
-
@element = nil
-
@value = value.is_a?(Array) ? value : value.to_s
-
end
-
-
1
def value
-
@value
-
end
-
-
1
def to_s
-
decoded.to_s
-
end
-
-
1
def default
-
decoded
-
end
-
-
1
def field_length
-
@length ||= "#{name}: #{encode(decoded)}".length
-
end
-
-
1
def responsible_for?( val )
-
name.to_s.casecmp(val.to_s) == 0
-
end
-
-
1
private
-
-
1
FILENAME_RE = /\b(filename|name)=([^;"\r\n]+\s[^;"\r\n]+)/
-
1
def ensure_filename_quoted(value)
-
if value.is_a?(String)
-
value.sub FILENAME_RE, '\1="\2"'
-
else
-
value
-
end
-
end
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
1
module Mail
-
1
module CommonMessageId # :nodoc:
-
1
def element
-
@element ||= Mail::MessageIdsElement.new(value) unless Utilities.blank?(value)
-
end
-
-
1
def parse(val = value)
-
unless Utilities.blank?(val)
-
@element = Mail::MessageIdsElement.new(val)
-
else
-
nil
-
end
-
end
-
-
1
def message_id
-
element.message_id if element
-
end
-
-
1
def message_ids
-
element.message_ids if element
-
end
-
-
1
def default
-
return nil unless message_ids
-
if message_ids.length == 1
-
message_ids[0]
-
else
-
message_ids
-
end
-
end
-
-
1
private
-
-
1
def do_encode(field_name)
-
%Q{#{field_name}: #{formated_message_ids("\r\n ")}\r\n}
-
end
-
-
1
def do_decode
-
formated_message_ids(' ')
-
end
-
-
1
def formated_message_ids(join)
-
message_ids.map{ |m| "<#{m}>" }.join(join) if message_ids
-
end
-
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
1
module Mail
-
-
# ParameterHash is an intelligent Hash that allows you to add
-
# parameter values including the MIME extension paramaters that
-
# have the name*0="blah", name*1="bleh" keys, and will just return
-
# a single key called name="blahbleh" and do any required un-encoding
-
# to make that happen
-
# Parameters are defined in RFC2045, split keys are in RFC2231
-
-
1
class ParameterHash < IndifferentHash
-
-
1
include Mail::Utilities
-
-
1
def [](key_name)
-
key_pattern = Regexp.escape(key_name.to_s)
-
pairs = []
-
exact = nil
-
each do |k,v|
-
if k =~ /^#{key_pattern}(\*|$)/i
-
if $1 == ASTERISK
-
pairs << [k, v]
-
else
-
exact = k
-
end
-
end
-
end
-
if pairs.empty? # Just dealing with a single value pair
-
super(exact || key_name)
-
else # Dealing with a multiple value pair or a single encoded value pair
-
string = pairs.sort { |a,b| a.first.to_s <=> b.first.to_s }.map { |v| v.last }.join('')
-
if mt = string.match(/([\w\-]+)?'(\w\w)?'(.*)/)
-
string = mt[3]
-
encoding = mt[1]
-
else
-
encoding = nil
-
end
-
Mail::Encodings.param_decode(string, encoding)
-
end
-
end
-
-
1
def encoded
-
map.sort_by { |a| a.first.to_s }.map! do |key_name, value|
-
unless value.ascii_only?
-
value = Mail::Encodings.param_encode(value)
-
key_name = "#{key_name}*"
-
end
-
%Q{#{key_name}=#{quote_token(value)}}
-
end.join(";\r\n\s")
-
end
-
-
1
def decoded
-
map.sort_by { |a| a.first.to_s }.map! do |key_name, value|
-
%Q{#{key_name}=#{quote_token(value)}}
-
end.join("; ")
-
end
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
#
-
#
-
#
-
1
module Mail
-
1
class ContentDescriptionField < UnstructuredField
-
-
1
FIELD_NAME = 'content-description'
-
1
CAPITALIZED_FIELD = 'Content-Description'
-
-
1
def initialize(value = nil, charset = 'utf-8')
-
self.charset = charset
-
super(CAPITALIZED_FIELD, value, charset)
-
self.parse
-
self
-
end
-
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
1
require 'mail/fields/common/parameter_hash'
-
-
1
module Mail
-
1
class ContentDispositionField < StructuredField
-
-
1
FIELD_NAME = 'content-disposition'
-
1
CAPITALIZED_FIELD = 'Content-Disposition'
-
-
1
def initialize(value = nil, charset = 'utf-8')
-
self.charset = charset
-
value = ensure_filename_quoted(value)
-
super(CAPITALIZED_FIELD, value, charset)
-
self.parse
-
self
-
end
-
-
1
def parse(val = value)
-
unless Utilities.blank?(val)
-
@element = Mail::ContentDispositionElement.new(val)
-
end
-
end
-
-
1
def element
-
@element ||= Mail::ContentDispositionElement.new(value)
-
end
-
-
1
def disposition_type
-
element.disposition_type
-
end
-
-
1
def parameters
-
@parameters = ParameterHash.new
-
element.parameters.each { |p| @parameters.merge!(p) } unless element.parameters.nil?
-
@parameters
-
end
-
-
1
def filename
-
case
-
when parameters['filename']
-
@filename = parameters['filename']
-
when parameters['name']
-
@filename = parameters['name']
-
else
-
@filename = nil
-
end
-
@filename
-
end
-
-
# TODO: Fix this up
-
1
def encoded
-
if parameters.length > 0
-
p = ";\r\n\s#{parameters.encoded}\r\n"
-
else
-
p = "\r\n"
-
end
-
"#{CAPITALIZED_FIELD}: #{disposition_type}" + p
-
end
-
-
1
def decoded
-
if parameters.length > 0
-
p = "; #{parameters.decoded}"
-
else
-
p = ""
-
end
-
"#{disposition_type}" + p
-
end
-
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
#
-
#
-
#
-
1
module Mail
-
1
class ContentIdField < StructuredField
-
-
1
FIELD_NAME = 'content-id'
-
1
CAPITALIZED_FIELD = "Content-ID"
-
-
1
def initialize(value = nil, charset = 'utf-8')
-
self.charset = charset
-
@uniq = 1
-
if Utilities.blank?(value)
-
value = generate_content_id
-
else
-
value = value.to_s
-
end
-
super(CAPITALIZED_FIELD, value, charset)
-
self.parse
-
self
-
end
-
-
1
def parse(val = value)
-
unless Utilities.blank?(val)
-
@element = Mail::MessageIdsElement.new(val)
-
end
-
end
-
-
1
def element
-
@element ||= Mail::MessageIdsElement.new(value)
-
end
-
-
1
def name
-
'Content-ID'
-
end
-
-
1
def content_id
-
element.message_id
-
end
-
-
1
def to_s
-
"<#{content_id}>"
-
end
-
-
# TODO: Fix this up
-
1
def encoded
-
"#{CAPITALIZED_FIELD}: #{to_s}\r\n"
-
end
-
-
1
def decoded
-
"#{to_s}"
-
end
-
-
1
private
-
-
1
def generate_content_id
-
"<#{Mail.random_tag}@#{::Socket.gethostname}.mail>"
-
end
-
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
#
-
#
-
#
-
1
module Mail
-
1
class ContentLocationField < StructuredField
-
-
1
FIELD_NAME = 'content-location'
-
1
CAPITALIZED_FIELD = 'Content-Location'
-
-
1
def initialize(value = nil, charset = 'utf-8')
-
self.charset = charset
-
super(CAPITALIZED_FIELD, value, charset)
-
self.parse
-
self
-
end
-
-
1
def parse(val = value)
-
unless Utilities.blank?(val)
-
@element = Mail::ContentLocationElement.new(val)
-
end
-
end
-
-
1
def element
-
@element ||= Mail::ContentLocationElement.new(value)
-
end
-
-
1
def location
-
element.location
-
end
-
-
# TODO: Fix this up
-
1
def encoded
-
"#{CAPITALIZED_FIELD}: #{location}\r\n"
-
end
-
-
1
def decoded
-
location
-
end
-
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
#
-
#
-
#
-
1
module Mail
-
1
class ContentTransferEncodingField < StructuredField
-
-
1
FIELD_NAME = 'content-transfer-encoding'
-
1
CAPITALIZED_FIELD = 'Content-Transfer-Encoding'
-
-
1
def initialize(value = nil, charset = 'utf-8')
-
self.charset = charset
-
value = '7bit' if value.to_s =~ /7-?bits?/i
-
value = '8bit' if value.to_s =~ /8-?bits?/i
-
super(CAPITALIZED_FIELD, value, charset)
-
self.parse
-
self
-
end
-
-
1
def parse(val = value)
-
unless Utilities.blank?(val)
-
@element = Mail::ContentTransferEncodingElement.new(val)
-
end
-
end
-
-
1
def element
-
@element ||= Mail::ContentTransferEncodingElement.new(value)
-
end
-
-
1
def encoding
-
element.encoding
-
end
-
-
# TODO: Fix this up
-
1
def encoded
-
"#{CAPITALIZED_FIELD}: #{encoding}\r\n"
-
end
-
-
1
def decoded
-
encoding
-
end
-
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
1
require 'mail/fields/common/parameter_hash'
-
-
1
module Mail
-
1
class ContentTypeField < StructuredField
-
-
1
FIELD_NAME = 'content-type'
-
1
CAPITALIZED_FIELD = 'Content-Type'
-
-
1
def initialize(value = nil, charset = 'utf-8')
-
self.charset = charset
-
if value.class == Array
-
@main_type = value[0]
-
@sub_type = value[1]
-
@parameters = ParameterHash.new.merge!(value.last)
-
else
-
@main_type = nil
-
@sub_type = nil
-
@parameters = nil
-
value = value.to_s
-
end
-
value = ensure_filename_quoted(value)
-
super(CAPITALIZED_FIELD, value, charset)
-
self.parse
-
self
-
end
-
-
1
def parse(val = value)
-
unless Utilities.blank?(val)
-
self.value = val
-
@element = nil
-
element
-
end
-
end
-
-
1
def element
-
begin
-
@element ||= Mail::ContentTypeElement.new(value)
-
rescue
-
attempt_to_clean
-
end
-
end
-
-
1
def attempt_to_clean
-
# Sanitize the value, handle special cases
-
@element ||= Mail::ContentTypeElement.new(sanatize(value))
-
rescue
-
# All else fails, just get the MIME media type
-
@element ||= Mail::ContentTypeElement.new(get_mime_type(value))
-
end
-
-
1
def main_type
-
@main_type ||= element.main_type
-
end
-
-
1
def sub_type
-
@sub_type ||= element.sub_type
-
end
-
-
1
def string
-
"#{main_type}/#{sub_type}"
-
end
-
-
1
def default
-
decoded
-
end
-
-
1
alias :content_type :string
-
-
1
def parameters
-
unless @parameters
-
@parameters = ParameterHash.new
-
element.parameters.each { |p| @parameters.merge!(p) }
-
end
-
@parameters
-
end
-
-
1
def ContentTypeField.with_boundary(type)
-
new("#{type}; boundary=#{generate_boundary}")
-
end
-
-
1
def ContentTypeField.generate_boundary
-
"--==_mimepart_#{Mail.random_tag}"
-
end
-
-
1
def value
-
if @value.class == Array
-
"#{@main_type}/#{@sub_type}; #{stringify(parameters)}"
-
else
-
@value
-
end
-
end
-
-
1
def stringify(params)
-
params.map { |k,v| "#{k}=#{Encodings.param_encode(v)}" }.join("; ")
-
end
-
-
1
def filename
-
case
-
when parameters['filename']
-
@filename = parameters['filename']
-
when parameters['name']
-
@filename = parameters['name']
-
else
-
@filename = nil
-
end
-
@filename
-
end
-
-
# TODO: Fix this up
-
1
def encoded
-
if parameters.length > 0
-
p = ";\r\n\s#{parameters.encoded}"
-
else
-
p = ""
-
end
-
"#{CAPITALIZED_FIELD}: #{content_type}#{p}\r\n"
-
end
-
-
1
def decoded
-
if parameters.length > 0
-
p = "; #{parameters.decoded}"
-
else
-
p = ""
-
end
-
"#{content_type}" + p
-
end
-
-
1
private
-
-
1
def method_missing(name, *args, &block)
-
if name.to_s =~ /(\w+)=/
-
self.parameters[$1] = args.first
-
@value = "#{content_type}; #{stringify(parameters)}"
-
else
-
super
-
end
-
end
-
-
# Various special cases from random emails found that I am not going to change
-
# the parser for
-
1
def sanatize( val )
-
-
# TODO: check if there are cases where whitespace is not a separator
-
val = val.
-
gsub(/\s*=\s*/,'='). # remove whitespaces around equal sign
-
gsub(/[; ]+/, '; '). #use '; ' as a separator (or EOL)
-
gsub(/;\s*$/,'') #remove trailing to keep examples below
-
-
if val =~ /(boundary=(\S*))/i
-
val = "#{$`.downcase}boundary=#{$2}#{$'.downcase}"
-
else
-
val.downcase!
-
end
-
-
case
-
when val.chomp =~ /^\s*([\w\-]+)\/([\w\-]+)\s*;\s?(ISO[\w\-]+)$/i
-
# Microsoft helper:
-
# Handles 'type/subtype;ISO-8559-1'
-
"#{$1}/#{$2}; charset=#{quote_atom($3)}"
-
when val.chomp =~ /^text;?$/i
-
# Handles 'text;' and 'text'
-
"text/plain;"
-
when val.chomp =~ /^(\w+);\s(.*)$/i
-
# Handles 'text; <parameters>'
-
"text/plain; #{$2}"
-
when val =~ /([\w\-]+\/[\w\-]+);\scharset="charset="(\w+)""/i
-
# Handles text/html; charset="charset="GB2312""
-
"#{$1}; charset=#{quote_atom($2)}"
-
when val =~ /([\w\-]+\/[\w\-]+);\s+(.*)/i
-
type = $1
-
# Handles misquoted param values
-
# e.g: application/octet-stream; name=archiveshelp1[1].htm
-
# and: audio/x-midi;\r\n\sname=Part .exe
-
params = $2.to_s.split(/\s+/)
-
params = params.map { |i| i.to_s.chomp.strip }
-
params = params.map { |i| i.split(/\s*\=\s*/, 2) }
-
params = params.map { |i| "#{i[0]}=#{dquote(i[1].to_s.gsub(/;$/,""))}" }.join('; ')
-
"#{type}; #{params}"
-
when val =~ /^\s*$/
-
'text/plain'
-
else
-
val
-
end
-
end
-
-
1
def get_mime_type( val )
-
case
-
when val =~ /^([\w\-]+)\/([\w\-]+);.+$/i
-
"#{$1}/#{$2}"
-
else
-
'text/plain'
-
end
-
end
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
#
-
# = Date Field
-
#
-
# The Date field inherits from StructuredField and handles the Date: header
-
# field in the email.
-
#
-
# Sending date to a mail message will instantiate a Mail::Field object that
-
# has a DateField as its field type. This includes all Mail::CommonAddress
-
# module instance methods.
-
#
-
# There must be excatly one Date field in an RFC2822 email.
-
#
-
# == Examples:
-
#
-
# mail = Mail.new
-
# mail.date = 'Mon, 24 Nov 1997 14:22:01 -0800'
-
# mail.date #=> #<DateTime: 211747170121/86400,-1/3,2299161>
-
# mail.date.to_s #=> 'Mon, 24 Nov 1997 14:22:01 -0800'
-
# mail[:date] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::DateField:0x180e1c4
-
# mail['date'] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::DateField:0x180e1c4
-
# mail['Date'] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::DateField:0x180e1c4
-
#
-
1
require 'mail/fields/common/common_date'
-
-
1
module Mail
-
1
class DateField < StructuredField
-
-
1
include Mail::CommonDate
-
-
1
FIELD_NAME = 'date'
-
1
CAPITALIZED_FIELD = "Date"
-
-
1
def initialize(value = nil, charset = 'utf-8')
-
self.charset = charset
-
if Utilities.blank?(value)
-
value = ::DateTime.now.strftime('%a, %d %b %Y %H:%M:%S %z')
-
else
-
value = value.to_s.gsub(/\(.*?\)/, '').squeeze(' ')
-
value = ::DateTime.parse(value).strftime('%a, %d %b %Y %H:%M:%S %z')
-
end
-
super(CAPITALIZED_FIELD, value, charset)
-
rescue ArgumentError => e
-
raise e unless "invalid date"==e.message
-
end
-
-
1
def encoded
-
do_encode(CAPITALIZED_FIELD)
-
end
-
-
1
def decoded
-
do_decode
-
end
-
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
#
-
# = From Field
-
#
-
# The From field inherits from StructuredField and handles the From: header
-
# field in the email.
-
#
-
# Sending from to a mail message will instantiate a Mail::Field object that
-
# has a FromField as its field type. This includes all Mail::CommonAddress
-
# module instance metods.
-
#
-
# Only one From field can appear in a header, though it can have multiple
-
# addresses and groups of addresses.
-
#
-
# == Examples:
-
#
-
# mail = Mail.new
-
# mail.from = 'Mikel Lindsaar <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail.from #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
# mail[:from] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::FromField:0x180e1c4
-
# mail['from'] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::FromField:0x180e1c4
-
# mail['From'] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::FromField:0x180e1c4
-
#
-
# mail[:from].encoded #=> 'from: Mikel Lindsaar <mikel@test.lindsaar.net>, ada@test.lindsaar.net\r\n'
-
# mail[:from].decoded #=> 'Mikel Lindsaar <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail[:from].addresses #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
# mail[:from].formatted #=> ['Mikel Lindsaar <mikel@test.lindsaar.net>', 'ada@test.lindsaar.net']
-
#
-
1
require 'mail/fields/common/common_address'
-
-
1
module Mail
-
1
class FromField < StructuredField
-
-
1
include Mail::CommonAddress
-
-
1
FIELD_NAME = 'from'
-
1
CAPITALIZED_FIELD = 'From'
-
-
1
def initialize(value = nil, charset = 'utf-8')
-
self.charset = charset
-
super(CAPITALIZED_FIELD, value, charset)
-
self
-
end
-
-
1
def encoded
-
do_encode(CAPITALIZED_FIELD)
-
end
-
-
1
def decoded
-
do_decode
-
end
-
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
#
-
# = In-Reply-To Field
-
#
-
# The In-Reply-To field inherits from StructuredField and handles the
-
# In-Reply-To: header field in the email.
-
#
-
# Sending in_reply_to to a mail message will instantiate a Mail::Field object that
-
# has a InReplyToField as its field type. This includes all Mail::CommonMessageId
-
# module instance metods.
-
#
-
# Note that, the #message_ids method will return an array of message IDs without the
-
# enclosing angle brackets which per RFC are not syntactically part of the message id.
-
#
-
# Only one InReplyTo field can appear in a header, though it can have multiple
-
# Message IDs.
-
#
-
# == Examples:
-
#
-
# mail = Mail.new
-
# mail.in_reply_to = '<F6E2D0B4-CC35-4A91-BA4C-C7C712B10C13@test.me.dom>'
-
# mail.in_reply_to #=> '<F6E2D0B4-CC35-4A91-BA4C-C7C712B10C13@test.me.dom>'
-
# mail[:in_reply_to] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::InReplyToField:0x180e1c4
-
# mail['in_reply_to'] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::InReplyToField:0x180e1c4
-
# mail['In-Reply-To'] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::InReplyToField:0x180e1c4
-
#
-
# mail[:in_reply_to].message_ids #=> ['F6E2D0B4-CC35-4A91-BA4C-C7C712B10C13@test.me.dom']
-
#
-
1
require 'mail/fields/common/common_message_id'
-
-
1
module Mail
-
1
class InReplyToField < StructuredField
-
-
1
include Mail::CommonMessageId
-
-
1
FIELD_NAME = 'in-reply-to'
-
1
CAPITALIZED_FIELD = 'In-Reply-To'
-
-
1
def initialize(value = nil, charset = 'utf-8')
-
self.charset = charset
-
value = value.join("\r\n\s") if value.is_a?(Array)
-
super(CAPITALIZED_FIELD, value, charset)
-
self.parse
-
self
-
end
-
-
1
def encoded
-
do_encode(CAPITALIZED_FIELD)
-
end
-
-
1
def decoded
-
do_decode
-
end
-
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
#
-
# keywords = "Keywords:" phrase *("," phrase) CRLF
-
1
module Mail
-
1
class KeywordsField < StructuredField
-
-
1
FIELD_NAME = 'keywords'
-
1
CAPITALIZED_FIELD = 'Keywords'
-
-
1
def initialize(value = nil, charset = 'utf-8')
-
self.charset = charset
-
super(CAPITALIZED_FIELD, value, charset)
-
self
-
end
-
-
1
def parse(val = value)
-
unless Utilities.blank?(val)
-
@phrase_list ||= PhraseList.new(value)
-
end
-
end
-
-
1
def phrase_list
-
@phrase_list ||= PhraseList.new(value)
-
end
-
-
1
def keywords
-
phrase_list.phrases
-
end
-
-
1
def encoded
-
"#{CAPITALIZED_FIELD}: #{keywords.join(",\r\n ")}\r\n"
-
end
-
-
1
def decoded
-
keywords.join(', ')
-
end
-
-
1
def default
-
keywords
-
end
-
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
#
-
# = Message-ID Field
-
#
-
# The Message-ID field inherits from StructuredField and handles the
-
# Message-ID: header field in the email.
-
#
-
# Sending message_id to a mail message will instantiate a Mail::Field object that
-
# has a MessageIdField as its field type. This includes all Mail::CommonMessageId
-
# module instance metods.
-
#
-
# Only one MessageId field can appear in a header, and syntactically it can only have
-
# one Message ID. The message_ids method call has been left in however as it will only
-
# return the one message id, ie, an array of length 1.
-
#
-
# Note that, the #message_ids method will return an array of message IDs without the
-
# enclosing angle brackets which per RFC are not syntactically part of the message id.
-
#
-
# == Examples:
-
#
-
# mail = Mail.new
-
# mail.message_id = '<F6E2D0B4-CC35-4A91-BA4C-C7C712B10C13@test.me.dom>'
-
# mail.message_id #=> '<F6E2D0B4-CC35-4A91-BA4C-C7C712B10C13@test.me.dom>'
-
# mail[:message_id] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::MessageIdField:0x180e1c4
-
# mail['message_id'] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::MessageIdField:0x180e1c4
-
# mail['Message-ID'] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::MessageIdField:0x180e1c4
-
#
-
# mail[:message_id].message_id #=> 'F6E2D0B4-CC35-4A91-BA4C-C7C712B10C13@test.me.dom'
-
# mail[:message_id].message_ids #=> ['F6E2D0B4-CC35-4A91-BA4C-C7C712B10C13@test.me.dom']
-
#
-
1
require 'mail/fields/common/common_message_id'
-
-
1
module Mail
-
1
class MessageIdField < StructuredField
-
-
1
include Mail::CommonMessageId
-
-
1
FIELD_NAME = 'message-id'
-
1
CAPITALIZED_FIELD = 'Message-ID'
-
-
1
def initialize(value = nil, charset = 'utf-8')
-
self.charset = charset
-
@uniq = 1
-
if Utilities.blank?(value)
-
self.name = CAPITALIZED_FIELD
-
self.value = generate_message_id
-
else
-
super(CAPITALIZED_FIELD, value, charset)
-
end
-
self.parse
-
self
-
-
end
-
-
1
def name
-
'Message-ID'
-
end
-
-
1
def message_ids
-
[message_id]
-
end
-
-
1
def to_s
-
"<#{message_id}>"
-
end
-
-
1
def encoded
-
do_encode(CAPITALIZED_FIELD)
-
end
-
-
1
def decoded
-
do_decode
-
end
-
-
1
private
-
-
1
def generate_message_id
-
"<#{Mail.random_tag}@#{::Socket.gethostname}.mail>"
-
end
-
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
#
-
#
-
#
-
1
module Mail
-
1
class MimeVersionField < StructuredField
-
-
1
FIELD_NAME = 'mime-version'
-
1
CAPITALIZED_FIELD = 'Mime-Version'
-
-
1
def initialize(value = nil, charset = 'utf-8')
-
self.charset = charset
-
if Utilities.blank?(value)
-
value = '1.0'
-
end
-
super(CAPITALIZED_FIELD, value, charset)
-
self.parse
-
self
-
-
end
-
-
1
def parse(val = value)
-
unless Utilities.blank?(val)
-
@element = Mail::MimeVersionElement.new(val)
-
end
-
end
-
-
1
def element
-
@element ||= Mail::MimeVersionElement.new(value)
-
end
-
-
1
def version
-
"#{element.major}.#{element.minor}"
-
end
-
-
1
def major
-
element.major.to_i
-
end
-
-
1
def minor
-
element.minor.to_i
-
end
-
-
1
def encoded
-
"#{CAPITALIZED_FIELD}: #{version}\r\n"
-
end
-
-
1
def decoded
-
version
-
end
-
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
#
-
# trace = [return]
-
# 1*received
-
#
-
# return = "Return-Path:" path CRLF
-
#
-
# path = ([CFWS] "<" ([CFWS] / addr-spec) ">" [CFWS]) /
-
# obs-path
-
#
-
# received = "Received:" name-val-list ";" date-time CRLF
-
#
-
# name-val-list = [CFWS] [name-val-pair *(CFWS name-val-pair)]
-
#
-
# name-val-pair = item-name CFWS item-value
-
#
-
# item-name = ALPHA *(["-"] (ALPHA / DIGIT))
-
#
-
# item-value = 1*angle-addr / addr-spec /
-
# atom / domain / msg-id
-
#
-
1
module Mail
-
1
class ReceivedField < StructuredField
-
-
1
FIELD_NAME = 'received'
-
1
CAPITALIZED_FIELD = 'Received'
-
-
1
def initialize(value = nil, charset = 'utf-8')
-
self.charset = charset
-
super(CAPITALIZED_FIELD, value, charset)
-
self.parse
-
self
-
-
end
-
-
1
def parse(val = value)
-
unless Utilities.blank?(val)
-
@element = Mail::ReceivedElement.new(val)
-
end
-
end
-
-
1
def element
-
@element ||= Mail::ReceivedElement.new(value)
-
end
-
-
1
def date_time
-
@datetime ||= ::DateTime.parse("#{element.date_time}")
-
end
-
-
1
def info
-
element.info
-
end
-
-
1
def formatted_date
-
date_time.strftime("%a, %d %b %Y %H:%M:%S ") + date_time.zone.delete(':')
-
end
-
-
1
def encoded
-
if Utilities.blank?(value)
-
"#{CAPITALIZED_FIELD}: \r\n"
-
else
-
"#{CAPITALIZED_FIELD}: #{info}; #{formatted_date}\r\n"
-
end
-
end
-
-
1
def decoded
-
if Utilities.blank?(value)
-
""
-
else
-
"#{info}; #{formatted_date}"
-
end
-
end
-
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
#
-
# = References Field
-
#
-
# The References field inherits references StructuredField and handles the References: header
-
# field in the email.
-
#
-
# Sending references to a mail message will instantiate a Mail::Field object that
-
# has a ReferencesField as its field type. This includes all Mail::CommonAddress
-
# module instance metods.
-
#
-
# Note that, the #message_ids method will return an array of message IDs without the
-
# enclosing angle brackets which per RFC are not syntactically part of the message id.
-
#
-
# Only one References field can appear in a header, though it can have multiple
-
# Message IDs.
-
#
-
# == Examples:
-
#
-
# mail = Mail.new
-
# mail.references = '<F6E2D0B4-CC35-4A91-BA4C-C7C712B10C13@test.me.dom>'
-
# mail.references #=> '<F6E2D0B4-CC35-4A91-BA4C-C7C712B10C13@test.me.dom>'
-
# mail[:references] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::ReferencesField:0x180e1c4
-
# mail['references'] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::ReferencesField:0x180e1c4
-
# mail['References'] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::ReferencesField:0x180e1c4
-
#
-
# mail[:references].message_ids #=> ['F6E2D0B4-CC35-4A91-BA4C-C7C712B10C13@test.me.dom']
-
#
-
1
require 'mail/fields/common/common_message_id'
-
-
1
module Mail
-
1
class ReferencesField < StructuredField
-
-
1
include CommonMessageId
-
-
1
FIELD_NAME = 'references'
-
1
CAPITALIZED_FIELD = 'References'
-
-
1
def initialize(value = nil, charset = 'utf-8')
-
self.charset = charset
-
value = value.join("\r\n\s") if value.is_a?(Array)
-
super(CAPITALIZED_FIELD, value, charset)
-
self.parse
-
self
-
end
-
-
1
def encoded
-
do_encode(CAPITALIZED_FIELD)
-
end
-
-
1
def decoded
-
do_decode
-
end
-
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
#
-
# = Reply-To Field
-
#
-
# The Reply-To field inherits reply-to StructuredField and handles the Reply-To: header
-
# field in the email.
-
#
-
# Sending reply_to to a mail message will instantiate a Mail::Field object that
-
# has a ReplyToField as its field type. This includes all Mail::CommonAddress
-
# module instance metods.
-
#
-
# Only one Reply-To field can appear in a header, though it can have multiple
-
# addresses and groups of addresses.
-
#
-
# == Examples:
-
#
-
# mail = Mail.new
-
# mail.reply_to = 'Mikel Lindsaar <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail.reply_to #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
# mail[:reply_to] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::ReplyToField:0x180e1c4
-
# mail['reply-to'] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::ReplyToField:0x180e1c4
-
# mail['Reply-To'] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::ReplyToField:0x180e1c4
-
#
-
# mail[:reply_to].encoded #=> 'Reply-To: Mikel Lindsaar <mikel@test.lindsaar.net>, ada@test.lindsaar.net\r\n'
-
# mail[:reply_to].decoded #=> 'Mikel Lindsaar <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail[:reply_to].addresses #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
# mail[:reply_to].formatted #=> ['Mikel Lindsaar <mikel@test.lindsaar.net>', 'ada@test.lindsaar.net']
-
#
-
1
require 'mail/fields/common/common_address'
-
-
1
module Mail
-
1
class ReplyToField < StructuredField
-
-
1
include Mail::CommonAddress
-
-
1
FIELD_NAME = 'reply-to'
-
1
CAPITALIZED_FIELD = 'Reply-To'
-
-
1
def initialize(value = nil, charset = 'utf-8')
-
self.charset = charset
-
super(CAPITALIZED_FIELD, value, charset)
-
self
-
end
-
-
1
def encoded
-
do_encode(CAPITALIZED_FIELD)
-
end
-
-
1
def decoded
-
do_decode
-
end
-
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
#
-
# = Resent-Bcc Field
-
#
-
# The Resent-Bcc field inherits resent-bcc StructuredField and handles the
-
# Resent-Bcc: header field in the email.
-
#
-
# Sending resent_bcc to a mail message will instantiate a Mail::Field object that
-
# has a ResentBccField as its field type. This includes all Mail::CommonAddress
-
# module instance metods.
-
#
-
# Only one Resent-Bcc field can appear in a header, though it can have multiple
-
# addresses and groups of addresses.
-
#
-
# == Examples:
-
#
-
# mail = Mail.new
-
# mail.resent_bcc = 'Mikel Lindsaar <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail.resent_bcc #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
# mail[:resent_bcc] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::ResentBccField:0x180e1c4
-
# mail['resent-bcc'] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::ResentBccField:0x180e1c4
-
# mail['Resent-Bcc'] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::ResentBccField:0x180e1c4
-
#
-
# mail[:resent_bcc].encoded #=> 'Resent-Bcc: Mikel Lindsaar <mikel@test.lindsaar.net>, ada@test.lindsaar.net\r\n'
-
# mail[:resent_bcc].decoded #=> 'Mikel Lindsaar <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail[:resent_bcc].addresses #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
# mail[:resent_bcc].formatted #=> ['Mikel Lindsaar <mikel@test.lindsaar.net>', 'ada@test.lindsaar.net']
-
#
-
1
require 'mail/fields/common/common_address'
-
-
1
module Mail
-
1
class ResentBccField < StructuredField
-
-
1
include Mail::CommonAddress
-
-
1
FIELD_NAME = 'resent-bcc'
-
1
CAPITALIZED_FIELD = 'Resent-Bcc'
-
-
1
def initialize(value = nil, charset = 'utf-8')
-
self.charset = charset
-
super(CAPITALIZED_FIELD, value, charset)
-
self
-
end
-
-
1
def encoded
-
do_encode(CAPITALIZED_FIELD)
-
end
-
-
1
def decoded
-
do_decode
-
end
-
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
#
-
# = Resent-Cc Field
-
#
-
# The Resent-Cc field inherits resent-cc StructuredField and handles the Resent-Cc: header
-
# field in the email.
-
#
-
# Sending resent_cc to a mail message will instantiate a Mail::Field object that
-
# has a ResentCcField as its field type. This includes all Mail::CommonAddress
-
# module instance metods.
-
#
-
# Only one Resent-Cc field can appear in a header, though it can have multiple
-
# addresses and groups of addresses.
-
#
-
# == Examples:
-
#
-
# mail = Mail.new
-
# mail.resent_cc = 'Mikel Lindsaar <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail.resent_cc #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
# mail[:resent_cc] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::ResentCcField:0x180e1c4
-
# mail['resent-cc'] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::ResentCcField:0x180e1c4
-
# mail['Resent-Cc'] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::ResentCcField:0x180e1c4
-
#
-
# mail[:resent_cc].encoded #=> 'Resent-Cc: Mikel Lindsaar <mikel@test.lindsaar.net>, ada@test.lindsaar.net\r\n'
-
# mail[:resent_cc].decoded #=> 'Mikel Lindsaar <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail[:resent_cc].addresses #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
# mail[:resent_cc].formatted #=> ['Mikel Lindsaar <mikel@test.lindsaar.net>', 'ada@test.lindsaar.net']
-
#
-
1
require 'mail/fields/common/common_address'
-
-
1
module Mail
-
1
class ResentCcField < StructuredField
-
-
1
include Mail::CommonAddress
-
-
1
FIELD_NAME = 'resent-cc'
-
1
CAPITALIZED_FIELD = 'Resent-Cc'
-
-
1
def initialize(value = nil, charset = 'utf-8')
-
self.charset = charset
-
super(CAPITALIZED_FIELD, value, charset)
-
self
-
end
-
-
1
def encoded
-
do_encode(CAPITALIZED_FIELD)
-
end
-
-
1
def decoded
-
do_decode
-
end
-
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
#
-
# resent-date = "Resent-Date:" date-time CRLF
-
1
require 'mail/fields/common/common_date'
-
-
1
module Mail
-
1
class ResentDateField < StructuredField
-
-
1
include Mail::CommonDate
-
-
1
FIELD_NAME = 'resent-date'
-
1
CAPITALIZED_FIELD = 'Resent-Date'
-
-
1
def initialize(value = nil, charset = 'utf-8')
-
self.charset = charset
-
if Utilities.blank?(value)
-
value = ::DateTime.now.strftime('%a, %d %b %Y %H:%M:%S %z')
-
else
-
value = ::DateTime.parse(value.to_s).strftime('%a, %d %b %Y %H:%M:%S %z')
-
end
-
super(CAPITALIZED_FIELD, value, charset)
-
self
-
end
-
-
1
def encoded
-
do_encode(CAPITALIZED_FIELD)
-
end
-
-
1
def decoded
-
do_decode
-
end
-
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
#
-
# = Resent-From Field
-
#
-
# The Resent-From field inherits resent-from StructuredField and handles the Resent-From: header
-
# field in the email.
-
#
-
# Sending resent_from to a mail message will instantiate a Mail::Field object that
-
# has a ResentFromField as its field type. This includes all Mail::CommonAddress
-
# module instance metods.
-
#
-
# Only one Resent-From field can appear in a header, though it can have multiple
-
# addresses and groups of addresses.
-
#
-
# == Examples:
-
#
-
# mail = Mail.new
-
# mail.resent_from = 'Mikel Lindsaar <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail.resent_from #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
# mail[:resent_from] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::ResentFromField:0x180e1c4
-
# mail['resent-from'] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::ResentFromField:0x180e1c4
-
# mail['Resent-From'] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::ResentFromField:0x180e1c4
-
#
-
# mail[:resent_from].encoded #=> 'Resent-From: Mikel Lindsaar <mikel@test.lindsaar.net>, ada@test.lindsaar.net\r\n'
-
# mail[:resent_from].decoded #=> 'Mikel Lindsaar <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail[:resent_from].addresses #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
# mail[:resent_from].formatted #=> ['Mikel Lindsaar <mikel@test.lindsaar.net>', 'ada@test.lindsaar.net']
-
#
-
1
require 'mail/fields/common/common_address'
-
-
1
module Mail
-
1
class ResentFromField < StructuredField
-
-
1
include Mail::CommonAddress
-
-
1
FIELD_NAME = 'resent-from'
-
1
CAPITALIZED_FIELD = 'Resent-From'
-
-
1
def initialize(value = nil, charset = 'utf-8')
-
self.charset = charset
-
super(CAPITALIZED_FIELD, value, charset)
-
self
-
end
-
-
1
def encoded
-
do_encode(CAPITALIZED_FIELD)
-
end
-
-
1
def decoded
-
do_decode
-
end
-
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
#
-
# resent-msg-id = "Resent-Message-ID:" msg-id CRLF
-
1
require 'mail/fields/common/common_message_id'
-
-
1
module Mail
-
1
class ResentMessageIdField < StructuredField
-
-
1
include CommonMessageId
-
-
1
FIELD_NAME = 'resent-message-id'
-
1
CAPITALIZED_FIELD = 'Resent-Message-ID'
-
-
1
def initialize(value = nil, charset = 'utf-8')
-
self.charset = charset
-
super(CAPITALIZED_FIELD, value, charset)
-
self.parse
-
self
-
end
-
-
1
def name
-
'Resent-Message-ID'
-
end
-
-
1
def encoded
-
do_encode(CAPITALIZED_FIELD)
-
end
-
-
1
def decoded
-
do_decode
-
end
-
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
#
-
# = Resent-Sender Field
-
#
-
# The Resent-Sender field inherits resent-sender StructuredField and handles the Resent-Sender: header
-
# field in the email.
-
#
-
# Sending resent_sender to a mail message will instantiate a Mail::Field object that
-
# has a ResentSenderField as its field type. This includes all Mail::CommonAddress
-
# module instance metods.
-
#
-
# Only one Resent-Sender field can appear in a header, though it can have multiple
-
# addresses and groups of addresses.
-
#
-
# == Examples:
-
#
-
# mail = Mail.new
-
# mail.resent_sender = 'Mikel Lindsaar <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail.resent_sender #=> ['mikel@test.lindsaar.net']
-
# mail[:resent_sender] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::ResentSenderField:0x180e1c4
-
# mail['resent-sender'] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::ResentSenderField:0x180e1c4
-
# mail['Resent-Sender'] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::ResentSenderField:0x180e1c4
-
#
-
# mail.resent_sender.to_s #=> 'Mikel Lindsaar <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail.resent_sender.addresses #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
# mail.resent_sender.formatted #=> ['Mikel Lindsaar <mikel@test.lindsaar.net>', 'ada@test.lindsaar.net']
-
#
-
1
require 'mail/fields/common/common_address'
-
-
1
module Mail
-
1
class ResentSenderField < StructuredField
-
-
1
include Mail::CommonAddress
-
-
1
FIELD_NAME = 'resent-sender'
-
1
CAPITALIZED_FIELD = 'Resent-Sender'
-
-
1
def initialize(value = nil, charset = 'utf-8')
-
self.charset = charset
-
super(CAPITALIZED_FIELD, value, charset)
-
self
-
end
-
-
1
def addresses
-
[address.address]
-
end
-
-
1
def address
-
address_list.addresses.first
-
end
-
-
1
def encoded
-
do_encode(CAPITALIZED_FIELD)
-
end
-
-
1
def decoded
-
do_decode
-
end
-
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
#
-
# = Resent-To Field
-
#
-
# The Resent-To field inherits resent-to StructuredField and handles the Resent-To: header
-
# field in the email.
-
#
-
# Sending resent_to to a mail message will instantiate a Mail::Field object that
-
# has a ResentToField as its field type. This includes all Mail::CommonAddress
-
# module instance metods.
-
#
-
# Only one Resent-To field can appear in a header, though it can have multiple
-
# addresses and groups of addresses.
-
#
-
# == Examples:
-
#
-
# mail = Mail.new
-
# mail.resent_to = 'Mikel Lindsaar <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail.resent_to #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
# mail[:resent_to] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::ResentToField:0x180e1c4
-
# mail['resent-to'] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::ResentToField:0x180e1c4
-
# mail['Resent-To'] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::ResentToField:0x180e1c4
-
#
-
# mail[:resent_to].encoded #=> 'Resent-To: Mikel Lindsaar <mikel@test.lindsaar.net>, ada@test.lindsaar.net\r\n'
-
# mail[:resent_to].decoded #=> 'Mikel Lindsaar <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail[:resent_to].addresses #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
# mail[:resent_to].formatted #=> ['Mikel Lindsaar <mikel@test.lindsaar.net>', 'ada@test.lindsaar.net']
-
#
-
1
require 'mail/fields/common/common_address'
-
-
1
module Mail
-
1
class ResentToField < StructuredField
-
-
1
include Mail::CommonAddress
-
-
1
FIELD_NAME = 'resent-to'
-
1
CAPITALIZED_FIELD = 'Resent-To'
-
-
1
def initialize(value = nil, charset = 'utf-8')
-
self.charset = charset
-
super(CAPITALIZED_FIELD, value, charset)
-
self
-
end
-
-
1
def encoded
-
do_encode(CAPITALIZED_FIELD)
-
end
-
-
1
def decoded
-
do_decode
-
end
-
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
#
-
# 4.4.3. REPLY-TO / RESENT-REPLY-TO
-
#
-
# Note: The "Return-Path" field is added by the mail transport
-
# service, at the time of final deliver. It is intended
-
# to identify a path back to the orginator of the mes-
-
# sage. The "Reply-To" field is added by the message
-
# originator and is intended to direct replies.
-
#
-
# trace = [return]
-
# 1*received
-
#
-
# return = "Return-Path:" path CRLF
-
#
-
# path = ([CFWS] "<" ([CFWS] / addr-spec) ">" [CFWS]) /
-
# obs-path
-
#
-
# received = "Received:" name-val-list ";" date-time CRLF
-
#
-
# name-val-list = [CFWS] [name-val-pair *(CFWS name-val-pair)]
-
#
-
# name-val-pair = item-name CFWS item-value
-
#
-
# item-name = ALPHA *(["-"] (ALPHA / DIGIT))
-
#
-
# item-value = 1*angle-addr / addr-spec /
-
# atom / domain / msg-id
-
#
-
1
require 'mail/fields/common/common_address'
-
-
1
module Mail
-
1
class ReturnPathField < StructuredField
-
-
1
include Mail::CommonAddress
-
-
1
FIELD_NAME = 'return-path'
-
1
CAPITALIZED_FIELD = 'Return-Path'
-
-
1
def initialize(value = nil, charset = 'utf-8')
-
value = nil if value == '<>'
-
self.charset = charset
-
super(CAPITALIZED_FIELD, value, charset)
-
self
-
end
-
-
1
def encoded
-
"#{CAPITALIZED_FIELD}: <#{address}>\r\n"
-
end
-
-
1
def decoded
-
do_decode
-
end
-
-
1
def address
-
addresses.first
-
end
-
-
1
def default
-
address
-
end
-
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
#
-
# = Sender Field
-
#
-
# The Sender field inherits sender StructuredField and handles the Sender: header
-
# field in the email.
-
#
-
# Sending sender to a mail message will instantiate a Mail::Field object that
-
# has a SenderField as its field type. This includes all Mail::CommonAddress
-
# module instance metods.
-
#
-
# Only one Sender field can appear in a header, though it can have multiple
-
# addresses and groups of addresses.
-
#
-
# == Examples:
-
#
-
# mail = Mail.new
-
# mail.sender = 'Mikel Lindsaar <mikel@test.lindsaar.net>'
-
# mail.sender #=> 'mikel@test.lindsaar.net'
-
# mail[:sender] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::SenderField:0x180e1c4
-
# mail['sender'] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::SenderField:0x180e1c4
-
# mail['Sender'] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::SenderField:0x180e1c4
-
#
-
# mail[:sender].encoded #=> "Sender: Mikel Lindsaar <mikel@test.lindsaar.net>\r\n"
-
# mail[:sender].decoded #=> 'Mikel Lindsaar <mikel@test.lindsaar.net>'
-
# mail[:sender].addresses #=> ['mikel@test.lindsaar.net']
-
# mail[:sender].formatted #=> ['Mikel Lindsaar <mikel@test.lindsaar.net>']
-
#
-
1
require 'mail/fields/common/common_address'
-
-
1
module Mail
-
1
class SenderField < StructuredField
-
-
1
include Mail::CommonAddress
-
-
1
FIELD_NAME = 'sender'
-
1
CAPITALIZED_FIELD = 'Sender'
-
-
1
def initialize(value = nil, charset = 'utf-8')
-
self.charset = charset
-
super(CAPITALIZED_FIELD, value, charset)
-
self
-
end
-
-
1
def addresses
-
[address.address]
-
end
-
-
1
def address
-
address_list.addresses.first
-
end
-
-
1
def encoded
-
do_encode(CAPITALIZED_FIELD)
-
end
-
-
1
def decoded
-
do_decode
-
end
-
-
1
def default
-
address.address
-
end
-
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
1
require 'mail/fields/common/common_field'
-
-
1
module Mail
-
# Provides access to a structured header field
-
#
-
# ===Per RFC 2822:
-
# 2.2.2. Structured Header Field Bodies
-
#
-
# Some field bodies in this standard have specific syntactical
-
# structure more restrictive than the unstructured field bodies
-
# described above. These are referred to as "structured" field bodies.
-
# Structured field bodies are sequences of specific lexical tokens as
-
# described in sections 3 and 4 of this standard. Many of these tokens
-
# are allowed (according to their syntax) to be introduced or end with
-
# comments (as described in section 3.2.3) as well as the space (SP,
-
# ASCII value 32) and horizontal tab (HTAB, ASCII value 9) characters
-
# (together known as the white space characters, WSP), and those WSP
-
# characters are subject to header "folding" and "unfolding" as
-
# described in section 2.2.3. Semantic analysis of structured field
-
# bodies is given along with their syntax.
-
1
class StructuredField
-
-
1
include Mail::CommonField
-
1
include Mail::Utilities
-
-
1
def initialize(name = nil, value = nil, charset = nil)
-
self.name = name
-
self.value = value
-
self.charset = charset
-
self
-
end
-
-
1
def charset
-
@charset
-
end
-
-
1
def charset=(val)
-
@charset = val
-
end
-
-
1
def default
-
decoded
-
end
-
-
1
def errors
-
[]
-
end
-
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
#
-
# subject = "Subject:" unstructured CRLF
-
1
module Mail
-
1
class SubjectField < UnstructuredField
-
-
1
FIELD_NAME = 'subject'
-
1
CAPITALIZED_FIELD = "Subject"
-
-
1
def initialize(value = nil, charset = 'utf-8')
-
self.charset = charset
-
super(CAPITALIZED_FIELD, value, charset)
-
end
-
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
#
-
# = To Field
-
#
-
# The To field inherits to StructuredField and handles the To: header
-
# field in the email.
-
#
-
# Sending to to a mail message will instantiate a Mail::Field object that
-
# has a ToField as its field type. This includes all Mail::CommonAddress
-
# module instance metods.
-
#
-
# Only one To field can appear in a header, though it can have multiple
-
# addresses and groups of addresses.
-
#
-
# == Examples:
-
#
-
# mail = Mail.new
-
# mail.to = 'Mikel Lindsaar <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail.to #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
# mail[:to] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::ToField:0x180e1c4
-
# mail['to'] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::ToField:0x180e1c4
-
# mail['To'] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::ToField:0x180e1c4
-
#
-
# mail[:to].encoded #=> 'To: Mikel Lindsaar <mikel@test.lindsaar.net>, ada@test.lindsaar.net\r\n'
-
# mail[:to].decoded #=> 'Mikel Lindsaar <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail[:to].addresses #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
# mail[:to].formatted #=> ['Mikel Lindsaar <mikel@test.lindsaar.net>', 'ada@test.lindsaar.net']
-
#
-
1
require 'mail/fields/common/common_address'
-
-
1
module Mail
-
1
class ToField < StructuredField
-
-
1
include Mail::CommonAddress
-
-
1
FIELD_NAME = 'to'
-
1
CAPITALIZED_FIELD = 'To'
-
-
1
def initialize(value = nil, charset = 'utf-8')
-
self.charset = charset
-
super(CAPITALIZED_FIELD, value, charset)
-
self
-
end
-
-
1
def encoded
-
do_encode(CAPITALIZED_FIELD)
-
end
-
-
1
def decoded
-
do_decode
-
end
-
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
1
require 'mail/fields/common/common_field'
-
-
1
module Mail
-
# Provides access to an unstructured header field
-
#
-
# ===Per RFC 2822:
-
# 2.2.1. Unstructured Header Field Bodies
-
#
-
# Some field bodies in this standard are defined simply as
-
# "unstructured" (which is specified below as any US-ASCII characters,
-
# except for CR and LF) with no further restrictions. These are
-
# referred to as unstructured field bodies. Semantically, unstructured
-
# field bodies are simply to be treated as a single line of characters
-
# with no further processing (except for header "folding" and
-
# "unfolding" as described in section 2.2.3).
-
1
class UnstructuredField
-
-
1
include Mail::CommonField
-
1
include Mail::Utilities
-
-
1
attr_accessor :charset
-
1
attr_reader :errors
-
-
1
def initialize(name, value, charset = nil)
-
@errors = []
-
-
if value.is_a?(Array)
-
# Probably has arrived here from a failed parse of an AddressList Field
-
value = value.join(', ')
-
else
-
# Ensure we are dealing with a string
-
value = value.to_s
-
-
# Mark UTF-8 strings parsed from ASCII-8BIT
-
if value.respond_to?(:force_encoding) && value.encoding == Encoding::ASCII_8BIT
-
utf8 = value.dup.force_encoding(Encoding::UTF_8)
-
value = utf8 if utf8.valid_encoding?
-
end
-
end
-
-
if charset
-
self.charset = charset
-
else
-
if value.respond_to?(:encoding)
-
self.charset = value.encoding
-
else
-
self.charset = $KCODE
-
end
-
end
-
self.name = name
-
self.value = value
-
self
-
end
-
-
1
def encoded
-
do_encode
-
end
-
-
1
def decoded
-
do_decode
-
end
-
-
1
def default
-
decoded
-
end
-
-
1
def parse # An unstructured field does not parse
-
self
-
end
-
-
1
private
-
-
1
def do_encode
-
if value && !value.empty?
-
"#{wrapped_value}\r\n"
-
else
-
''
-
end
-
end
-
-
1
def do_decode
-
Utilities.blank?(value) ? nil : Encodings.decode_encode(value, :decode)
-
end
-
-
# 2.2.3. Long Header Fields
-
#
-
# Each header field is logically a single line of characters comprising
-
# the field name, the colon, and the field body. For convenience
-
# however, and to deal with the 998/78 character limitations per line,
-
# the field body portion of a header field can be split into a multiple
-
# line representation; this is called "folding". The general rule is
-
# that wherever this standard allows for folding white space (not
-
# simply WSP characters), a CRLF may be inserted before any WSP. For
-
# example, the header field:
-
#
-
# Subject: This is a test
-
#
-
# can be represented as:
-
#
-
# Subject: This
-
# is a test
-
#
-
# Note: Though structured field bodies are defined in such a way that
-
# folding can take place between many of the lexical tokens (and even
-
# within some of the lexical tokens), folding SHOULD be limited to
-
# placing the CRLF at higher-level syntactic breaks. For instance, if
-
# a field body is defined as comma-separated values, it is recommended
-
# that folding occur after the comma separating the structured items in
-
# preference to other places where the field could be folded, even if
-
# it is allowed elsewhere.
-
1
def wrapped_value # :nodoc:
-
wrap_lines(name, fold("#{name}: ".length))
-
end
-
-
# 6.2. Display of 'encoded-word's
-
#
-
# When displaying a particular header field that contains multiple
-
# 'encoded-word's, any 'linear-white-space' that separates a pair of
-
# adjacent 'encoded-word's is ignored. (This is to allow the use of
-
# multiple 'encoded-word's to represent long strings of unencoded text,
-
# without having to separate 'encoded-word's where spaces occur in the
-
# unencoded text.)
-
1
def wrap_lines(name, folded_lines)
-
result = ["#{name}: #{folded_lines.shift}"]
-
result.concat(folded_lines)
-
result.join("\r\n\s")
-
end
-
-
1
def fold(prepend = 0) # :nodoc:
-
encoding = normalized_encoding
-
decoded_string = decoded.to_s
-
should_encode = !decoded_string.ascii_only?
-
if should_encode
-
first = true
-
words = decoded_string.split(/[ \t]/).map do |word|
-
if first
-
first = !first
-
else
-
word = " #{word}"
-
end
-
if !word.ascii_only?
-
word
-
else
-
word.scan(/.{7}|.+$/)
-
end
-
end.flatten
-
else
-
words = decoded_string.split(/[ \t]/)
-
end
-
-
folded_lines = []
-
while !words.empty?
-
limit = 78 - prepend
-
limit = limit - 7 - encoding.length if should_encode
-
line = String.new
-
first_word = true
-
while !words.empty?
-
break unless word = words.first.dup
-
-
# Convert on 1.9+ only since we aren't sure of the current
-
# charset encoding on 1.8. We'd need to track internal/external
-
# charset on each field.
-
if charset && word.respond_to?(:encoding)
-
word = Encodings.transcode_charset(word, word.encoding, charset)
-
end
-
-
word = encode(word) if should_encode
-
word = encode_crlf(word)
-
# Skip to next line if we're going to go past the limit
-
# Unless this is the first word, in which case we're going to add it anyway
-
# Note: This means that a word that's longer than 998 characters is going to break the spec. Please fix if this is a problem for you.
-
# (The fix, it seems, would be to use encoded-word encoding on it, because that way you can break it across multiple lines and
-
# the linebreak will be ignored)
-
break if !line.empty? && (line.length + word.length + 1 > limit)
-
# Remove the word from the queue ...
-
words.shift
-
# Add word separator
-
if first_word
-
first_word = false
-
else
-
line << " " if !should_encode
-
end
-
-
# ... add it in encoded form to the current line
-
line << word
-
end
-
# Encode the line if necessary
-
line = "=?#{encoding}?Q?#{line}?=" if should_encode
-
# Add the line to the output and reset the prepend
-
folded_lines << line
-
prepend = 0
-
end
-
folded_lines
-
end
-
-
1
def encode(value)
-
value = [value].pack(CAPITAL_M).gsub(EQUAL_LF, EMPTY)
-
value.gsub!(/"/, '=22')
-
value.gsub!(/\(/, '=28')
-
value.gsub!(/\)/, '=29')
-
value.gsub!(/\?/, '=3F')
-
value.gsub!(/_/, '=5F')
-
value.gsub!(/ /, '_')
-
value
-
end
-
-
1
def encode_crlf(value)
-
value.gsub!(CR, CR_ENCODED)
-
value.gsub!(LF, LF_ENCODED)
-
value
-
end
-
-
1
def normalized_encoding
-
encoding = charset.to_s.upcase.gsub('_', '-')
-
encoding = 'UTF-8' if encoding == 'UTF8' # Ruby 1.8.x and $KCODE == 'u'
-
encoding
-
end
-
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
1
module Mail
-
-
# Provides access to a header object.
-
#
-
# ===Per RFC2822
-
#
-
# 2.2. Header Fields
-
#
-
# Header fields are lines composed of a field name, followed by a colon
-
# (":"), followed by a field body, and terminated by CRLF. A field
-
# name MUST be composed of printable US-ASCII characters (i.e.,
-
# characters that have values between 33 and 126, inclusive), except
-
# colon. A field body may be composed of any US-ASCII characters,
-
# except for CR and LF. However, a field body may contain CRLF when
-
# used in header "folding" and "unfolding" as described in section
-
# 2.2.3. All field bodies MUST conform to the syntax described in
-
# sections 3 and 4 of this standard.
-
1
class Header
-
1
include Constants
-
1
include Utilities
-
1
include Enumerable
-
-
1
@@maximum_amount = 1000
-
-
# Large amount of headers in Email might create extra high CPU load
-
# Use this parameter to limit number of headers that will be parsed by
-
# mail library.
-
# Default: 1000
-
1
def self.maximum_amount
-
@@maximum_amount
-
end
-
-
1
def self.maximum_amount=(value)
-
@@maximum_amount = value
-
end
-
-
# Creates a new header object.
-
#
-
# Accepts raw text or nothing. If given raw text will attempt to parse
-
# it and split it into the various fields, instantiating each field as
-
# it goes.
-
#
-
# If it finds a field that should be a structured field (such as content
-
# type), but it fails to parse it, it will simply make it an unstructured
-
# field and leave it alone. This will mean that the data is preserved but
-
# no automatic processing of that field will happen. If you find one of
-
# these cases, please make a patch and send it in, or at the least, send
-
# me the example so we can fix it.
-
1
def initialize(header_text = nil, charset = nil)
-
@charset = charset
-
self.raw_source = header_text
-
split_header if header_text
-
end
-
-
1
def initialize_copy(original)
-
super
-
@fields = @fields.dup
-
@fields.map!(&:dup)
-
end
-
-
# The preserved raw source of the header as you passed it in, untouched
-
# for your Regexing glory.
-
1
def raw_source
-
@raw_source
-
end
-
-
# Returns an array of all the fields in the header in order that they
-
# were read in.
-
1
def fields
-
@fields ||= FieldList.new
-
end
-
-
# 3.6. Field definitions
-
#
-
# It is important to note that the header fields are not guaranteed to
-
# be in a particular order. They may appear in any order, and they
-
# have been known to be reordered occasionally when transported over
-
# the Internet. However, for the purposes of this standard, header
-
# fields SHOULD NOT be reordered when a message is transported or
-
# transformed. More importantly, the trace header fields and resent
-
# header fields MUST NOT be reordered, and SHOULD be kept in blocks
-
# prepended to the message. See sections 3.6.6 and 3.6.7 for more
-
# information.
-
#
-
# Populates the fields container with Field objects in the order it
-
# receives them in.
-
#
-
# Acceps an array of field string values, for example:
-
#
-
# h = Header.new
-
# h.fields = ['From: mikel@me.com', 'To: bob@you.com']
-
1
def fields=(unfolded_fields)
-
@fields = Mail::FieldList.new
-
Kernel.warn "WARNING: More than #{self.class.maximum_amount} header fields; only using the first #{self.class.maximum_amount} and ignoring the rest" if unfolded_fields.length > self.class.maximum_amount
-
unfolded_fields[0..(self.class.maximum_amount-1)].each do |field|
-
-
if field = Field.parse(field, charset)
-
if limited_field?(field.name) && (selected = select_field_for(field.name)) && selected.any?
-
selected.first.update(field.name, field.value)
-
else
-
@fields << field
-
end
-
end
-
end
-
-
end
-
-
1
def errors
-
@fields.map(&:errors).flatten(1)
-
end
-
-
# 3.6. Field definitions
-
#
-
# The following table indicates limits on the number of times each
-
# field may occur in a message header as well as any special
-
# limitations on the use of those fields. An asterisk next to a value
-
# in the minimum or maximum column indicates that a special restriction
-
# appears in the Notes column.
-
#
-
# <snip table from 3.6>
-
#
-
# As per RFC, many fields can appear more than once, we will return a string
-
# of the value if there is only one header, or if there is more than one
-
# matching header, will return an array of values in order that they appear
-
# in the header ordered from top to bottom.
-
#
-
# Example:
-
#
-
# h = Header.new
-
# h.fields = ['To: mikel@me.com', 'X-Mail-SPAM: 15', 'X-Mail-SPAM: 20']
-
# h['To'] #=> 'mikel@me.com'
-
# h['X-Mail-SPAM'] #=> ['15', '20']
-
1
def [](name)
-
name = dasherize(name)
-
name.downcase!
-
selected = select_field_for(name)
-
case
-
when selected.length > 1
-
selected.map { |f| f }
-
when !Utilities.blank?(selected)
-
selected.first
-
else
-
nil
-
end
-
end
-
-
# Sets the FIRST matching field in the header to passed value, or deletes
-
# the FIRST field matched from the header if passed nil
-
#
-
# Example:
-
#
-
# h = Header.new
-
# h.fields = ['To: mikel@me.com', 'X-Mail-SPAM: 15', 'X-Mail-SPAM: 20']
-
# h['To'] = 'bob@you.com'
-
# h['To'] #=> 'bob@you.com'
-
# h['X-Mail-SPAM'] = '10000'
-
# h['X-Mail-SPAM'] # => ['15', '20', '10000']
-
# h['X-Mail-SPAM'] = nil
-
# h['X-Mail-SPAM'] # => nil
-
1
def []=(name, value)
-
name = dasherize(name)
-
if name.include?(':')
-
raise ArgumentError, "Header names may not contain a colon: #{name.inspect}"
-
end
-
fn = name.downcase
-
selected = select_field_for(fn)
-
-
case
-
# User wants to delete the field
-
when !Utilities.blank?(selected) && value == nil
-
fields.delete_if { |f| selected.include?(f) }
-
-
# User wants to change the field
-
when !Utilities.blank?(selected) && limited_field?(fn)
-
selected.first.update(fn, value)
-
-
# User wants to create the field
-
else
-
# Need to insert in correct order for trace fields
-
self.fields << Field.new(name.to_s, value, charset)
-
end
-
if dasherize(fn) == "content-type"
-
# Update charset if specified in Content-Type
-
params = self[:content_type].parameters rescue nil
-
@charset = params[:charset] if params && params[:charset]
-
end
-
end
-
-
1
def charset
-
@charset
-
end
-
-
1
def charset=(val)
-
params = self[:content_type].parameters rescue nil
-
if params
-
params[:charset] = val
-
end
-
@charset = val
-
end
-
-
1
LIMITED_FIELDS = %w[ date from sender reply-to to cc bcc
-
message-id in-reply-to references subject
-
return-path content-type mime-version
-
content-transfer-encoding content-description
-
content-id content-disposition content-location]
-
-
1
def encoded
-
buffer = String.new
-
buffer.force_encoding('us-ascii') if buffer.respond_to?(:force_encoding)
-
fields.each do |field|
-
buffer << field.encoded
-
end
-
buffer
-
end
-
-
1
def to_s
-
encoded
-
end
-
-
1
def decoded
-
raise NoMethodError, 'Can not decode an entire header as there could be character set conflicts, try calling #decoded on the various fields.'
-
end
-
-
1
def field_summary
-
fields.map { |f| "<#{f.name}: #{f.value}>" }.join(", ")
-
end
-
-
# Returns true if the header has a Message-ID defined (empty or not)
-
1
def has_message_id?
-
!fields.select { |f| f.responsible_for?('Message-ID') }.empty?
-
end
-
-
# Returns true if the header has a Content-ID defined (empty or not)
-
1
def has_content_id?
-
!fields.select { |f| f.responsible_for?('Content-ID') }.empty?
-
end
-
-
# Returns true if the header has a Date defined (empty or not)
-
1
def has_date?
-
!fields.select { |f| f.responsible_for?('Date') }.empty?
-
end
-
-
# Returns true if the header has a MIME version defined (empty or not)
-
1
def has_mime_version?
-
!fields.select { |f| f.responsible_for?('Mime-Version') }.empty?
-
end
-
-
1
private
-
-
1
def raw_source=(val)
-
@raw_source = ::Mail::Utilities.to_crlf(val).lstrip
-
end
-
-
# Splits an unfolded and line break cleaned header into individual field
-
# strings.
-
1
def split_header
-
self.fields = raw_source.split(HEADER_SPLIT)
-
end
-
-
1
def select_field_for(name)
-
fields.select { |f| f.responsible_for?(name) }
-
end
-
-
1
def limited_field?(name)
-
LIMITED_FIELDS.include?(name.to_s.downcase)
-
end
-
-
# Enumerable support; yield each field in order to the block if there is one,
-
# or return an Enumerator for them if there isn't.
-
1
def each( &block )
-
return self.fields.each( &block ) if block
-
self.fields.each
-
end
-
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
-
# This is an almost cut and paste from ActiveSupport v3.0.6, copied in here so that Mail
-
# itself does not depend on ActiveSupport to avoid versioning conflicts
-
-
1
module Mail
-
1
class IndifferentHash < Hash
-
-
1
def initialize(constructor = {})
-
if constructor.is_a?(Hash)
-
super()
-
update(constructor)
-
else
-
super(constructor)
-
end
-
end
-
-
1
def default(key = nil)
-
if key.is_a?(Symbol) && include?(key = key.to_s)
-
self[key]
-
else
-
super
-
end
-
end
-
-
1
def self.new_from_hash_copying_default(hash)
-
IndifferentHash.new(hash).tap do |new_hash|
-
new_hash.default = hash.default
-
end
-
end
-
-
1
alias_method :regular_writer, :[]= unless method_defined?(:regular_writer)
-
1
alias_method :regular_update, :update unless method_defined?(:regular_update)
-
-
# Assigns a new value to the hash:
-
#
-
# hash = HashWithIndifferentAccess.new
-
# hash[:key] = "value"
-
#
-
1
def []=(key, value)
-
regular_writer(convert_key(key), convert_value(value))
-
end
-
-
1
alias_method :store, :[]=
-
-
# Updates the instantized hash with values from the second:
-
#
-
# hash_1 = HashWithIndifferentAccess.new
-
# hash_1[:key] = "value"
-
#
-
# hash_2 = HashWithIndifferentAccess.new
-
# hash_2[:key] = "New Value!"
-
#
-
# hash_1.update(hash_2) # => {"key"=>"New Value!"}
-
#
-
1
def update(other_hash)
-
other_hash.each_pair { |key, value| regular_writer(convert_key(key), convert_value(value)) }
-
self
-
end
-
-
1
alias_method :merge!, :update
-
-
# Checks the hash for a key matching the argument passed in:
-
#
-
# hash = HashWithIndifferentAccess.new
-
# hash["key"] = "value"
-
# hash.key? :key # => true
-
# hash.key? "key" # => true
-
#
-
1
def key?(key)
-
super(convert_key(key))
-
end
-
-
1
alias_method :include?, :key?
-
1
alias_method :has_key?, :key?
-
1
alias_method :member?, :key?
-
-
# Fetches the value for the specified key, same as doing hash[key]
-
1
def fetch(key, *extras)
-
super(convert_key(key), *extras)
-
end
-
-
# Returns an array of the values at the specified indices:
-
#
-
# hash = HashWithIndifferentAccess.new
-
# hash[:a] = "x"
-
# hash[:b] = "y"
-
# hash.values_at("a", "b") # => ["x", "y"]
-
#
-
1
def values_at(*indices)
-
indices.collect {|key| self[convert_key(key)]}
-
end
-
-
# Returns an exact copy of the hash.
-
1
def dup
-
IndifferentHash.new(self)
-
end
-
-
# Merges the instantized and the specified hashes together, giving precedence to the values from the second hash
-
# Does not overwrite the existing hash.
-
1
def merge(hash)
-
self.dup.update(hash)
-
end
-
-
# Performs the opposite of merge, with the keys and values from the first hash taking precedence over the second.
-
# This overloaded definition prevents returning a regular hash, if reverse_merge is called on a HashWithDifferentAccess.
-
1
def reverse_merge(other_hash)
-
super self.class.new_from_hash_copying_default(other_hash)
-
end
-
-
1
def reverse_merge!(other_hash)
-
replace(reverse_merge( other_hash ))
-
end
-
-
# Removes a specified key from the hash.
-
1
def delete(key)
-
super(convert_key(key))
-
end
-
-
1
def stringify_keys!; self end
-
1
def stringify_keys; dup end
-
1
def symbolize_keys; to_hash.symbolize_keys end
-
1
def to_options!; self end
-
-
1
def to_hash
-
Hash.new(default).merge!(self)
-
end
-
-
1
protected
-
-
1
def convert_key(key)
-
key.kind_of?(Symbol) ? key.to_s : key
-
end
-
-
1
def convert_value(value)
-
if value.class == Hash
-
self.class.new_from_hash_copying_default(value)
-
elsif value.is_a?(Array)
-
value.dup.replace(value.map { |e| convert_value(e) })
-
else
-
value
-
end
-
end
-
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
1
module Mail
-
-
# Allows you to create a new Mail::Message object.
-
#
-
# You can make an email via passing a string or passing a block.
-
#
-
# For example, the following two examples will create the same email
-
# message:
-
#
-
# Creating via a string:
-
#
-
# string = "To: mikel@test.lindsaar.net\r\n"
-
# string << "From: bob@test.lindsaar.net\r\n"
-
# string << "Subject: This is an email\r\n"
-
# string << "\r\n"
-
# string << "This is the body"
-
# Mail.new(string)
-
#
-
# Or creating via a block:
-
#
-
# message = Mail.new do
-
# to 'mikel@test.lindsaar.net'
-
# from 'bob@test.lindsaar.net'
-
# subject 'This is an email'
-
# body 'This is the body'
-
# end
-
#
-
# Or creating via a hash (or hash like object):
-
#
-
# message = Mail.new({:to => 'mikel@test.lindsaar.net',
-
# 'from' => 'bob@test.lindsaar.net',
-
# :subject => 'This is an email',
-
# :body => 'This is the body' })
-
#
-
# Note, the hash keys can be strings or symbols, the passed in object
-
# does not need to be a hash, it just needs to respond to :each_pair
-
# and yield each key value pair.
-
#
-
# As a side note, you can also create a new email through creating
-
# a Mail::Message object directly and then passing in values via string,
-
# symbol or direct method calls. See Mail::Message for more information.
-
#
-
# mail = Mail.new
-
# mail.to = 'mikel@test.lindsaar.net'
-
# mail[:from] = 'bob@test.lindsaar.net'
-
# mail['subject'] = 'This is an email'
-
# mail.body = 'This is the body'
-
1
def self.new(*args, &block)
-
Message.new(args, &block)
-
end
-
-
# Sets the default delivery method and retriever method for all new Mail objects.
-
# The delivery_method and retriever_method default to :smtp and :pop3, with defaults
-
# set.
-
#
-
# So sending a new email, if you have an SMTP server running on localhost is
-
# as easy as:
-
#
-
# Mail.deliver do
-
# to 'mikel@test.lindsaar.net'
-
# from 'bob@test.lindsaar.net'
-
# subject 'hi there!'
-
# body 'this is a body'
-
# end
-
#
-
# If you do not specify anything, you will get the following equivalent code set in
-
# every new mail object:
-
#
-
# Mail.defaults do
-
# delivery_method :smtp, { :address => "localhost",
-
# :port => 25,
-
# :domain => 'localhost.localdomain',
-
# :user_name => nil,
-
# :password => nil,
-
# :authentication => nil,
-
# :enable_starttls_auto => true }
-
#
-
# retriever_method :pop3, { :address => "localhost",
-
# :port => 995,
-
# :user_name => nil,
-
# :password => nil,
-
# :enable_ssl => true }
-
# end
-
#
-
# Mail.delivery_method.new #=> Mail::SMTP instance
-
# Mail.retriever_method.new #=> Mail::POP3 instance
-
#
-
# Each mail object inherits the default set in Mail.delivery_method, however, on
-
# a per email basis, you can override the method:
-
#
-
# mail.delivery_method :smtp
-
#
-
# Or you can override the method and pass in settings:
-
#
-
# mail.delivery_method :smtp, :address => 'some.host'
-
1
def self.defaults(&block)
-
Configuration.instance.instance_eval(&block)
-
end
-
-
# Returns the delivery method selected, defaults to an instance of Mail::SMTP
-
1
def self.delivery_method
-
Configuration.instance.delivery_method
-
end
-
-
# Returns the retriever method selected, defaults to an instance of Mail::POP3
-
1
def self.retriever_method
-
Configuration.instance.retriever_method
-
end
-
-
# Send an email using the default configuration. You do need to set a default
-
# configuration first before you use self.deliver, if you don't, an appropriate
-
# error will be raised telling you to.
-
#
-
# If you do not specify a delivery type, SMTP will be used.
-
#
-
# Mail.deliver do
-
# to 'mikel@test.lindsaar.net'
-
# from 'ada@test.lindsaar.net'
-
# subject 'This is a test email'
-
# body 'Not much to say here'
-
# end
-
#
-
# You can also do:
-
#
-
# mail = Mail.read('email.eml')
-
# mail.deliver!
-
#
-
# And your email object will be created and sent.
-
1
def self.deliver(*args, &block)
-
mail = self.new(args, &block)
-
mail.deliver
-
mail
-
end
-
-
# Find emails from the default retriever
-
# See Mail::Retriever for a complete documentation.
-
1
def self.find(*args, &block)
-
retriever_method.find(*args, &block)
-
end
-
-
# Finds and then deletes retrieved emails from the default retriever
-
# See Mail::Retriever for a complete documentation.
-
1
def self.find_and_delete(*args, &block)
-
retriever_method.find_and_delete(*args, &block)
-
end
-
-
# Receive the first email(s) from the default retriever
-
# See Mail::Retriever for a complete documentation.
-
1
def self.first(*args, &block)
-
retriever_method.first(*args, &block)
-
end
-
-
# Receive the first email(s) from the default retriever
-
# See Mail::Retriever for a complete documentation.
-
1
def self.last(*args, &block)
-
retriever_method.last(*args, &block)
-
end
-
-
# Receive all emails from the default retriever
-
# See Mail::Retriever for a complete documentation.
-
1
def self.all(*args, &block)
-
retriever_method.all(*args, &block)
-
end
-
-
# Reads in an email message from a path and instantiates it as a new Mail::Message
-
1
def self.read(filename)
-
self.new(File.open(filename, 'rb') { |f| f.read })
-
end
-
-
# Delete all emails from the default retriever
-
# See Mail::Retriever for a complete documentation.
-
1
def self.delete_all(*args, &block)
-
retriever_method.delete_all(*args, &block)
-
end
-
-
# Instantiates a new Mail::Message using a string
-
1
def Mail.read_from_string(mail_as_string)
-
Mail.new(mail_as_string)
-
end
-
-
1
def Mail.connection(&block)
-
retriever_method.connection(&block)
-
end
-
-
# Initialize the observers and interceptors arrays
-
1
@@delivery_notification_observers = []
-
1
@@delivery_interceptors = []
-
-
# You can register an object to be informed of every email that is sent through
-
# this method.
-
#
-
# Your object needs to respond to a single method #delivered_email(mail)
-
# which receives the email that is sent.
-
1
def self.register_observer(observer)
-
unless @@delivery_notification_observers.include?(observer)
-
@@delivery_notification_observers << observer
-
end
-
end
-
-
# Unregister the given observer, allowing mail to resume operations
-
# without it.
-
1
def self.unregister_observer(observer)
-
@@delivery_notification_observers.delete(observer)
-
end
-
-
# You can register an object to be given every mail object that will be sent,
-
# before it is sent. So if you want to add special headers or modify any
-
# email that gets sent through the Mail library, you can do so.
-
#
-
# Your object needs to respond to a single method #delivering_email(mail)
-
# which receives the email that is about to be sent. Make your modifications
-
# directly to this object.
-
1
def self.register_interceptor(interceptor)
-
unless @@delivery_interceptors.include?(interceptor)
-
@@delivery_interceptors << interceptor
-
end
-
end
-
-
# Unregister the given interceptor, allowing mail to resume operations
-
# without it.
-
1
def self.unregister_interceptor(interceptor)
-
@@delivery_interceptors.delete(interceptor)
-
end
-
-
1
def self.inform_observers(mail)
-
@@delivery_notification_observers.each do |observer|
-
observer.delivered_email(mail)
-
end
-
end
-
-
1
def self.inform_interceptors(mail)
-
@@delivery_interceptors.each do |interceptor|
-
interceptor.delivering_email(mail)
-
end
-
end
-
-
1
protected
-
-
1
RANDOM_TAG='%x%x_%x%x%d%x'
-
-
1
def self.random_tag
-
t = Time.now
-
sprintf(RANDOM_TAG,
-
t.to_i, t.tv_usec,
-
$$, Thread.current.object_id.abs, self.uniq, rand(255))
-
end
-
-
1
private
-
-
1
def self.something_random
-
1
(Thread.current.object_id * rand(255) / Time.now.to_f).to_s.slice(-3..-1).to_i
-
end
-
-
1
def self.uniq
-
@@uniq += 1
-
end
-
-
1
@@uniq = self.something_random
-
-
end
-
# frozen_string_literal: true
-
1
module Mail
-
1
module Matchers
-
1
def any_attachment
-
AnyAttachmentMatcher.new
-
end
-
-
1
def an_attachment_with_filename(filename)
-
AttachmentFilenameMatcher.new(filename)
-
end
-
-
1
class AnyAttachmentMatcher
-
1
def ===(other)
-
other.attachment?
-
end
-
end
-
-
1
class AttachmentFilenameMatcher
-
1
attr_reader :filename
-
1
def initialize(filename)
-
@filename = filename
-
end
-
-
1
def ===(other)
-
other.attachment? && other.filename == filename
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
module Mail
-
1
module Matchers
-
1
def have_sent_email
-
HasSentEmailMatcher.new(self)
-
end
-
-
1
class HasSentEmailMatcher
-
1
def initialize(_context)
-
end
-
-
1
def matches?(subject)
-
matching_deliveries = filter_matched_deliveries(Mail::TestMailer.deliveries)
-
!(matching_deliveries.empty?)
-
end
-
-
1
def from(sender)
-
@sender = sender
-
self
-
end
-
-
1
def to(recipient_or_list)
-
@recipients ||= []
-
-
if recipient_or_list.kind_of?(Array)
-
@recipients += recipient_or_list
-
else
-
@recipients << recipient_or_list
-
end
-
self
-
end
-
-
1
def cc(recipient_or_list)
-
@copy_recipients ||= []
-
-
if recipient_or_list.kind_of?(Array)
-
@copy_recipients += recipient_or_list
-
else
-
@copy_recipients << recipient_or_list
-
end
-
self
-
end
-
-
1
def bcc(recipient_or_list)
-
@blind_copy_recipients ||= []
-
@blind_copy_recipients.concat(Array(recipient_or_list))
-
self
-
end
-
-
1
def with_attachments(attachments)
-
@attachments ||= []
-
@attachments.concat(Array(attachments))
-
self
-
end
-
-
1
def with_no_attachments
-
@having_attachments = false
-
self
-
end
-
-
1
def with_any_attachments
-
@having_attachments = true
-
self
-
end
-
-
1
def with_subject(subject)
-
@subject = subject
-
self
-
end
-
-
1
def matching_subject(subject_matcher)
-
@subject_matcher = subject_matcher
-
self
-
end
-
-
1
def with_body(body)
-
@body = body
-
self
-
end
-
-
1
def matching_body(body_matcher)
-
@body_matcher = body_matcher
-
self
-
end
-
-
1
def with_html(body)
-
@html_part_body = body
-
self
-
end
-
-
1
def with_text(body)
-
@text_part_body = body
-
self
-
end
-
-
1
def description
-
result = "send a matching email"
-
result
-
end
-
-
1
def failure_message
-
result = "Expected email to be sent "
-
result += explain_expectations
-
result += dump_deliveries
-
result
-
end
-
-
1
def failure_message_when_negated
-
result = "Expected no email to be sent "
-
result += explain_expectations
-
result += dump_deliveries
-
result
-
end
-
-
1
protected
-
-
1
def filter_matched_deliveries(deliveries)
-
candidate_deliveries = deliveries
-
modifiers =
-
%w(sender recipients copy_recipients blind_copy_recipients subject
-
subject_matcher body body_matcher html_part_body text_part_body having_attachments attachments)
-
modifiers.each do |modifier_name|
-
next unless instance_variable_defined?("@#{modifier_name}")
-
candidate_deliveries = candidate_deliveries.select{|matching_delivery| self.send("matches_on_#{modifier_name}?", matching_delivery)}
-
end
-
-
candidate_deliveries
-
end
-
-
1
def matches_on_sender?(delivery)
-
delivery.from.include?(@sender)
-
end
-
-
1
def matches_on_recipients?(delivery)
-
@recipients.all? {|recipient| delivery.to.include?(recipient) }
-
end
-
-
1
def matches_on_copy_recipients?(delivery)
-
@copy_recipients.all? {|recipient| delivery.cc.include?(recipient) }
-
end
-
-
1
def matches_on_blind_copy_recipients?(delivery)
-
@blind_copy_recipients.all? {|recipient| delivery.bcc.include?(recipient) }
-
end
-
-
1
def matches_on_subject?(delivery)
-
delivery.subject == @subject
-
end
-
-
1
def matches_on_subject_matcher?(delivery)
-
@subject_matcher.match delivery.subject
-
end
-
-
1
def matches_on_having_attachments?(delivery)
-
@having_attachments && delivery.attachments.any? ||
-
(!@having_attachments && delivery.attachments.none?)
-
end
-
-
1
def matches_on_attachments?(delivery)
-
@attachments.each_with_index.inject( true ) do |sent_attachments, (attachment, index)|
-
sent_attachments &&= (attachment === delivery.attachments[index])
-
end
-
end
-
-
1
def matches_on_body?(delivery)
-
delivery.body == @body
-
end
-
-
1
def matches_on_body_matcher?(delivery)
-
@body_matcher.match delivery.body.raw_source
-
end
-
-
1
def matches_on_html_part_body?(delivery)
-
delivery.html_part.body == @html_part_body
-
end
-
-
1
def matches_on_text_part_body?(delivery)
-
delivery.text_part.body == @text_part_body
-
end
-
-
1
def explain_expectations
-
result = ''
-
result += "from #{@sender} " if instance_variable_defined?('@sender')
-
result += "to #{@recipients.inspect} " if instance_variable_defined?('@recipients')
-
result += "cc #{@copy_recipients.inspect} " if instance_variable_defined?('@copy_recipients')
-
result += "bcc #{@blind_copy_recipients.inspect} " if instance_variable_defined?('@blind_copy_recipients')
-
result += "with subject \"#{@subject}\" " if instance_variable_defined?('@subject')
-
result += "with subject matching \"#{@subject_matcher}\" " if instance_variable_defined?('@subject_matcher')
-
result += "with body \"#{@body}\" " if instance_variable_defined?('@body')
-
result += "with body matching \"#{@body_matcher}\" " if instance_variable_defined?('@body_matcher')
-
result += "with a text part matching \"#{@text_part_body}\" " if instance_variable_defined?('@text_part_body')
-
result += "with an HTML part matching \"#{@html_part_body}\" " if instance_variable_defined?('@html_part_body')
-
result
-
end
-
-
1
def dump_deliveries
-
"(actual deliveries: " + Mail::TestMailer.deliveries.inspect + ")"
-
end
-
end
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
1
require "yaml"
-
-
1
module Mail
-
# The Message class provides a single point of access to all things to do with an
-
# email message.
-
#
-
# You create a new email message by calling the Mail::Message.new method, or just
-
# Mail.new
-
#
-
# A Message object by default has the following objects inside it:
-
#
-
# * A Header object which contains all information and settings of the header of the email
-
# * Body object which contains all parts of the email that are not part of the header, this
-
# includes any attachments, body text, MIME parts etc.
-
#
-
# ==Per RFC2822
-
#
-
# 2.1. General Description
-
#
-
# At the most basic level, a message is a series of characters. A
-
# message that is conformant with this standard is comprised of
-
# characters with values in the range 1 through 127 and interpreted as
-
# US-ASCII characters [ASCII]. For brevity, this document sometimes
-
# refers to this range of characters as simply "US-ASCII characters".
-
#
-
# Note: This standard specifies that messages are made up of characters
-
# in the US-ASCII range of 1 through 127. There are other documents,
-
# specifically the MIME document series [RFC2045, RFC2046, RFC2047,
-
# RFC2048, RFC2049], that extend this standard to allow for values
-
# outside of that range. Discussion of those mechanisms is not within
-
# the scope of this standard.
-
#
-
# Messages are divided into lines of characters. A line is a series of
-
# characters that is delimited with the two characters carriage-return
-
# and line-feed; that is, the carriage return (CR) character (ASCII
-
# value 13) followed immediately by the line feed (LF) character (ASCII
-
# value 10). (The carriage-return/line-feed pair is usually written in
-
# this document as "CRLF".)
-
#
-
# A message consists of header fields (collectively called "the header
-
# of the message") followed, optionally, by a body. The header is a
-
# sequence of lines of characters with special syntax as defined in
-
# this standard. The body is simply a sequence of characters that
-
# follows the header and is separated from the header by an empty line
-
# (i.e., a line with nothing preceding the CRLF).
-
1
class Message
-
-
1
include Constants
-
1
include Utilities
-
-
# ==Making an email
-
#
-
# You can make an new mail object via a block, passing a string, file or direct assignment.
-
#
-
# ===Making an email via a block
-
#
-
# mail = Mail.new do |m|
-
# m.from 'mikel@test.lindsaar.net'
-
# m.to 'you@test.lindsaar.net'
-
# m.subject 'This is a test email'
-
# m.body File.read('body.txt')
-
# end
-
#
-
# mail.to_s #=> "From: mikel@test.lindsaar.net\r\nTo: you@...
-
#
-
# If may also pass a block with no arguments, in which case it will
-
# be evaluated in the scope of the new message instance:
-
#
-
# mail = Mail.new do
-
# from 'mikel@test.lindsaar.net'
-
# # ���
-
# end
-
#
-
# ===Making an email via passing a string
-
#
-
# mail = Mail.new("To: mikel@test.lindsaar.net\r\nSubject: Hello\r\n\r\nHi there!")
-
# mail.body.to_s #=> 'Hi there!'
-
# mail.subject #=> 'Hello'
-
# mail.to #=> 'mikel@test.lindsaar.net'
-
#
-
# ===Making an email from a file
-
#
-
# mail = Mail.read('path/to/file.eml')
-
# mail.body.to_s #=> 'Hi there!'
-
# mail.subject #=> 'Hello'
-
# mail.to #=> 'mikel@test.lindsaar.net'
-
#
-
# ===Making an email via assignment
-
#
-
# You can assign values to a mail object via four approaches:
-
#
-
# * Message#field_name=(value)
-
# * Message#field_name(value)
-
# * Message#['field_name']=(value)
-
# * Message#[:field_name]=(value)
-
#
-
# Examples:
-
#
-
# mail = Mail.new
-
# mail['from'] = 'mikel@test.lindsaar.net'
-
# mail[:to] = 'you@test.lindsaar.net'
-
# mail.subject 'This is a test email'
-
# mail.body = 'This is a body'
-
#
-
# mail.to_s #=> "From: mikel@test.lindsaar.net\r\nTo: you@...
-
#
-
1
def initialize(*args, &block)
-
@body = nil
-
@body_raw = nil
-
@separate_parts = false
-
@text_part = nil
-
@html_part = nil
-
@errors = nil
-
@header = nil
-
@charset = self.class.default_charset
-
@defaulted_charset = true
-
-
@smtp_envelope_from = nil
-
@smtp_envelope_to = nil
-
-
@perform_deliveries = true
-
@raise_delivery_errors = true
-
-
@delivery_handler = nil
-
-
@delivery_method = Mail.delivery_method.dup
-
-
@transport_encoding = Mail::Encodings.get_encoding('7bit')
-
-
@mark_for_delete = false
-
-
if args.flatten.first.respond_to?(:each_pair)
-
init_with_hash(args.flatten.first)
-
else
-
init_with_string(args.flatten[0].to_s)
-
end
-
-
# Support both builder styles:
-
#
-
# Mail.new do
-
# to 'recipient@example.com'
-
# end
-
#
-
# and
-
#
-
# Mail.new do |m|
-
# m.to 'recipient@example.com'
-
# end
-
if block_given?
-
if block.arity.zero? || (RUBY_VERSION < '1.9' && block.arity < 1)
-
instance_eval(&block)
-
else
-
yield self
-
end
-
end
-
-
self
-
end
-
-
# If you assign a delivery handler, mail will call :deliver_mail on the
-
# object you assign to delivery_handler, it will pass itself as the
-
# single argument.
-
#
-
# If you define a delivery_handler, then you are responsible for the
-
# following actions in the delivery cycle:
-
#
-
# * Appending the mail object to Mail.deliveries as you see fit.
-
# * Checking the mail.perform_deliveries flag to decide if you should
-
# actually call :deliver! the mail object or not.
-
# * Checking the mail.raise_delivery_errors flag to decide if you
-
# should raise delivery errors if they occur.
-
# * Actually calling :deliver! (with the bang) on the mail object to
-
# get it to deliver itself.
-
#
-
# A simplest implementation of a delivery_handler would be
-
#
-
# class MyObject
-
#
-
# def initialize
-
# @mail = Mail.new('To: mikel@test.lindsaar.net')
-
# @mail.delivery_handler = self
-
# end
-
#
-
# attr_accessor :mail
-
#
-
# def deliver_mail(mail)
-
# yield
-
# end
-
# end
-
#
-
# Then doing:
-
#
-
# obj = MyObject.new
-
# obj.mail.deliver
-
#
-
# Would cause Mail to call obj.deliver_mail passing itself as a parameter,
-
# which then can just yield and let Mail do its own private do_delivery
-
# method.
-
1
attr_accessor :delivery_handler
-
-
# If set to false, mail will go through the motions of doing a delivery,
-
# but not actually call the delivery method or append the mail object to
-
# the Mail.deliveries collection. Useful for testing.
-
#
-
# Mail.deliveries.size #=> 0
-
# mail.delivery_method :smtp
-
# mail.perform_deliveries = false
-
# mail.deliver # Mail::SMTP not called here
-
# Mail.deliveries.size #=> 0
-
#
-
# If you want to test and query the Mail.deliveries collection to see what
-
# mail you sent, you should set perform_deliveries to true and use
-
# the :test mail delivery_method:
-
#
-
# Mail.deliveries.size #=> 0
-
# mail.delivery_method :test
-
# mail.perform_deliveries = true
-
# mail.deliver
-
# Mail.deliveries.size #=> 1
-
#
-
# This setting is ignored by mail (though still available as a flag) if you
-
# define a delivery_handler
-
1
attr_accessor :perform_deliveries
-
-
# If set to false, mail will silently catch and ignore any exceptions
-
# raised through attempting to deliver an email.
-
#
-
# This setting is ignored by mail (though still available as a flag) if you
-
# define a delivery_handler
-
1
attr_accessor :raise_delivery_errors
-
-
1
def self.default_charset; @@default_charset; end
-
2
def self.default_charset=(charset); @@default_charset = charset; end
-
1
self.default_charset = 'UTF-8'
-
-
1
def register_for_delivery_notification(observer)
-
warn("Message#register_for_delivery_notification is deprecated, please call Mail.register_observer instead")
-
Mail.register_observer(observer)
-
end
-
-
1
def inform_observers
-
Mail.inform_observers(self)
-
end
-
-
1
def inform_interceptors
-
Mail.inform_interceptors(self)
-
end
-
-
# Delivers a mail object.
-
#
-
# Examples:
-
#
-
# mail = Mail.read('file.eml')
-
# mail.deliver
-
1
def deliver
-
inform_interceptors
-
if delivery_handler
-
delivery_handler.deliver_mail(self) { do_delivery }
-
else
-
do_delivery
-
end
-
inform_observers
-
self
-
end
-
-
# This method bypasses checking perform_deliveries and raise_delivery_errors,
-
# so use with caution.
-
#
-
# It still however fires off the interceptors and calls the observers callbacks if they are defined.
-
#
-
# Returns self
-
1
def deliver!
-
inform_interceptors
-
response = delivery_method.deliver!(self)
-
inform_observers
-
delivery_method.settings[:return_response] ? response : self
-
end
-
-
1
def delivery_method(method = nil, settings = {})
-
unless method
-
@delivery_method
-
else
-
@delivery_method = Configuration.instance.lookup_delivery_method(method).new(settings)
-
end
-
end
-
-
1
def reply(*args, &block)
-
self.class.new.tap do |reply|
-
if message_id
-
bracketed_message_id = "<#{message_id}>"
-
reply.in_reply_to = bracketed_message_id
-
if !references.nil?
-
refs = [references].flatten.map { |r| "<#{r}>" }
-
refs << bracketed_message_id
-
reply.references = refs.join(' ')
-
elsif !in_reply_to.nil? && !in_reply_to.kind_of?(Array)
-
reply.references = "<#{in_reply_to}> #{bracketed_message_id}"
-
end
-
reply.references ||= bracketed_message_id
-
end
-
if subject
-
reply.subject = subject =~ /^Re:/i ? subject : "RE: #{subject}"
-
end
-
if reply_to || from
-
reply.to = self[reply_to ? :reply_to : :from].to_s
-
end
-
if to
-
reply.from = self[:to].formatted.first.to_s
-
end
-
-
unless args.empty?
-
if args.flatten.first.respond_to?(:each_pair)
-
reply.send(:init_with_hash, args.flatten.first)
-
else
-
reply.send(:init_with_string, args.flatten[0].to_s.strip)
-
end
-
end
-
-
if block_given?
-
reply.instance_eval(&block)
-
end
-
end
-
end
-
-
# Provides the operator needed for sort et al.
-
#
-
# Compares this mail object with another mail object, this is done by date, so an
-
# email that is older than another will appear first.
-
#
-
# Example:
-
#
-
# mail1 = Mail.new do
-
# date(Time.now)
-
# end
-
# mail2 = Mail.new do
-
# date(Time.now - 86400) # 1 day older
-
# end
-
# [mail2, mail1].sort #=> [mail2, mail1]
-
1
def <=>(other)
-
if other.nil?
-
1
-
else
-
self.date <=> other.date
-
end
-
end
-
-
# Two emails are the same if they have the same fields and body contents. One
-
# gotcha here is that Mail will insert Message-IDs when calling encoded, so doing
-
# mail1.encoded == mail2.encoded is most probably not going to return what you think
-
# as the assigned Message-IDs by Mail (if not already defined as the same) will ensure
-
# that the two objects are unique, and this comparison will ALWAYS return false.
-
#
-
# So the == operator has been defined like so: Two messages are the same if they have
-
# the same content, ignoring the Message-ID field, unless BOTH emails have a defined and
-
# different Message-ID value, then they are false.
-
#
-
# So, in practice the == operator works like this:
-
#
-
# m1 = Mail.new("Subject: Hello\r\n\r\nHello")
-
# m2 = Mail.new("Subject: Hello\r\n\r\nHello")
-
# m1 == m2 #=> true
-
#
-
# m1 = Mail.new("Subject: Hello\r\n\r\nHello")
-
# m2 = Mail.new("Message-ID: <1234@test>\r\nSubject: Hello\r\n\r\nHello")
-
# m1 == m2 #=> true
-
#
-
# m1 = Mail.new("Message-ID: <1234@test>\r\nSubject: Hello\r\n\r\nHello")
-
# m2 = Mail.new("Subject: Hello\r\n\r\nHello")
-
# m1 == m2 #=> true
-
#
-
# m1 = Mail.new("Message-ID: <1234@test>\r\nSubject: Hello\r\n\r\nHello")
-
# m2 = Mail.new("Message-ID: <1234@test>\r\nSubject: Hello\r\n\r\nHello")
-
# m1 == m2 #=> true
-
#
-
# m1 = Mail.new("Message-ID: <1234@test>\r\nSubject: Hello\r\n\r\nHello")
-
# m2 = Mail.new("Message-ID: <DIFFERENT@test>\r\nSubject: Hello\r\n\r\nHello")
-
# m1 == m2 #=> false
-
1
def ==(other)
-
return false unless other.respond_to?(:encoded)
-
-
if self.message_id && other.message_id
-
self.encoded == other.encoded
-
else
-
dup.tap { |m| m.message_id = '<temp@test>' }.encoded ==
-
other.dup.tap { |m| m.message_id = '<temp@test>' }.encoded
-
end
-
end
-
-
1
def initialize_copy(original)
-
super
-
@header = @header.dup
-
end
-
-
# Provides access to the raw source of the message as it was when it
-
# was instantiated. This is set at initialization and so is untouched
-
# by the parsers or decoder / encoders
-
#
-
# Example:
-
#
-
# mail = Mail.new('This is an invalid email message')
-
# mail.raw_source #=> "This is an invalid email message"
-
1
def raw_source
-
@raw_source
-
end
-
-
# Sets the envelope from for the email
-
1
def set_envelope( val )
-
@raw_envelope = val
-
@envelope = Mail::Envelope.new( val )
-
end
-
-
# The raw_envelope is the From mikel@test.lindsaar.net Mon May 2 16:07:05 2009
-
# type field that you can see at the top of any email that has come
-
# from a mailbox
-
1
def raw_envelope
-
@raw_envelope
-
end
-
-
1
def envelope_from
-
@envelope ? @envelope.from : nil
-
end
-
-
1
def envelope_date
-
@envelope ? @envelope.date : nil
-
end
-
-
# Sets the header of the message object.
-
#
-
# Example:
-
#
-
# mail.header = 'To: mikel@test.lindsaar.net\r\nFrom: Bob@bob.com'
-
# mail.header #=> <#Mail::Header
-
1
def header=(value)
-
@header = Mail::Header.new(value, charset)
-
end
-
-
# Returns the header object of the message object. Or, if passed
-
# a parameter sets the value.
-
#
-
# Example:
-
#
-
# mail = Mail::Message.new('To: mikel\r\nFrom: you')
-
# mail.header #=> #<Mail::Header:0x13ce14 @raw_source="To: mikel\r\nFr...
-
#
-
# mail.header #=> nil
-
# mail.header 'To: mikel\r\nFrom: you'
-
# mail.header #=> #<Mail::Header:0x13ce14 @raw_source="To: mikel\r\nFr...
-
1
def header(value = nil)
-
value ? self.header = value : @header
-
end
-
-
# Provides a way to set custom headers, by passing in a hash
-
1
def headers(hash = {})
-
hash.each_pair do |k,v|
-
header[k] = v
-
end
-
end
-
-
# Returns a list of parser errors on the header, each field that had an error
-
# will be reparsed as an unstructured field to preserve the data inside, but
-
# will not be used for further processing.
-
#
-
# It returns a nested array of [field_name, value, original_error_message]
-
# per error found.
-
#
-
# Example:
-
#
-
# message = Mail.new("Content-Transfer-Encoding: weirdo\r\n")
-
# message.errors.size #=> 1
-
# message.errors.first[0] #=> "Content-Transfer-Encoding"
-
# message.errors.first[1] #=> "weirdo"
-
# message.errors.first[3] #=> <The original error message exception>
-
#
-
# This is a good first defence on detecting spam by the way. Some spammers send
-
# invalid emails to try and get email parsers to give up parsing them.
-
1
def errors
-
header.errors
-
end
-
-
# Returns the Bcc value of the mail object as an array of strings of
-
# address specs.
-
#
-
# Example:
-
#
-
# mail.bcc = 'Mikel <mikel@test.lindsaar.net>'
-
# mail.bcc #=> ['mikel@test.lindsaar.net']
-
# mail.bcc = 'Mikel <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail.bcc #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
#
-
# Also allows you to set the value by passing a value as a parameter
-
#
-
# Example:
-
#
-
# mail.bcc 'Mikel <mikel@test.lindsaar.net>'
-
# mail.bcc #=> ['mikel@test.lindsaar.net']
-
#
-
# Additionally, you can append new addresses to the returned Array like
-
# object.
-
#
-
# Example:
-
#
-
# mail.bcc 'Mikel <mikel@test.lindsaar.net>'
-
# mail.bcc << 'ada@test.lindsaar.net'
-
# mail.bcc #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
1
def bcc( val = nil )
-
default :bcc, val
-
end
-
-
# Sets the Bcc value of the mail object, pass in a string of the field
-
#
-
# Example:
-
#
-
# mail.bcc = 'Mikel <mikel@test.lindsaar.net>'
-
# mail.bcc #=> ['mikel@test.lindsaar.net']
-
# mail.bcc = 'Mikel <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail.bcc #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
1
def bcc=( val )
-
header[:bcc] = val
-
end
-
-
# Returns the Cc value of the mail object as an array of strings of
-
# address specs.
-
#
-
# Example:
-
#
-
# mail.cc = 'Mikel <mikel@test.lindsaar.net>'
-
# mail.cc #=> ['mikel@test.lindsaar.net']
-
# mail.cc = 'Mikel <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail.cc #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
#
-
# Also allows you to set the value by passing a value as a parameter
-
#
-
# Example:
-
#
-
# mail.cc 'Mikel <mikel@test.lindsaar.net>'
-
# mail.cc #=> ['mikel@test.lindsaar.net']
-
#
-
# Additionally, you can append new addresses to the returned Array like
-
# object.
-
#
-
# Example:
-
#
-
# mail.cc 'Mikel <mikel@test.lindsaar.net>'
-
# mail.cc << 'ada@test.lindsaar.net'
-
# mail.cc #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
1
def cc( val = nil )
-
default :cc, val
-
end
-
-
# Sets the Cc value of the mail object, pass in a string of the field
-
#
-
# Example:
-
#
-
# mail.cc = 'Mikel <mikel@test.lindsaar.net>'
-
# mail.cc #=> ['mikel@test.lindsaar.net']
-
# mail.cc = 'Mikel <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail.cc #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
1
def cc=( val )
-
header[:cc] = val
-
end
-
-
1
def comments( val = nil )
-
default :comments, val
-
end
-
-
1
def comments=( val )
-
header[:comments] = val
-
end
-
-
1
def content_description( val = nil )
-
default :content_description, val
-
end
-
-
1
def content_description=( val )
-
header[:content_description] = val
-
end
-
-
1
def content_disposition( val = nil )
-
default :content_disposition, val
-
end
-
-
1
def content_disposition=( val )
-
header[:content_disposition] = val
-
end
-
-
1
def content_id( val = nil )
-
default :content_id, val
-
end
-
-
1
def content_id=( val )
-
header[:content_id] = val
-
end
-
-
1
def content_location( val = nil )
-
default :content_location, val
-
end
-
-
1
def content_location=( val )
-
header[:content_location] = val
-
end
-
-
1
def content_transfer_encoding( val = nil )
-
default :content_transfer_encoding, val
-
end
-
-
1
def content_transfer_encoding=( val )
-
header[:content_transfer_encoding] = val
-
end
-
-
1
def content_type( val = nil )
-
default :content_type, val
-
end
-
-
1
def content_type=( val )
-
header[:content_type] = val
-
end
-
-
1
def date( val = nil )
-
default :date, val
-
end
-
-
1
def date=( val )
-
header[:date] = val
-
end
-
-
1
def transport_encoding( val = nil)
-
if val
-
self.transport_encoding = val
-
else
-
@transport_encoding
-
end
-
end
-
-
1
def transport_encoding=( val )
-
@transport_encoding = Mail::Encodings.get_encoding(val)
-
end
-
-
# Returns the From value of the mail object as an array of strings of
-
# address specs.
-
#
-
# Example:
-
#
-
# mail.from = 'Mikel <mikel@test.lindsaar.net>'
-
# mail.from #=> ['mikel@test.lindsaar.net']
-
# mail.from = 'Mikel <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail.from #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
#
-
# Also allows you to set the value by passing a value as a parameter
-
#
-
# Example:
-
#
-
# mail.from 'Mikel <mikel@test.lindsaar.net>'
-
# mail.from #=> ['mikel@test.lindsaar.net']
-
#
-
# Additionally, you can append new addresses to the returned Array like
-
# object.
-
#
-
# Example:
-
#
-
# mail.from 'Mikel <mikel@test.lindsaar.net>'
-
# mail.from << 'ada@test.lindsaar.net'
-
# mail.from #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
1
def from( val = nil )
-
default :from, val
-
end
-
-
# Sets the From value of the mail object, pass in a string of the field
-
#
-
# Example:
-
#
-
# mail.from = 'Mikel <mikel@test.lindsaar.net>'
-
# mail.from #=> ['mikel@test.lindsaar.net']
-
# mail.from = 'Mikel <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail.from #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
1
def from=( val )
-
header[:from] = val
-
end
-
-
1
def in_reply_to( val = nil )
-
default :in_reply_to, val
-
end
-
-
1
def in_reply_to=( val )
-
header[:in_reply_to] = val
-
end
-
-
1
def keywords( val = nil )
-
default :keywords, val
-
end
-
-
1
def keywords=( val )
-
header[:keywords] = val
-
end
-
-
# Returns the Message-ID of the mail object. Note, per RFC 2822 the Message ID
-
# consists of what is INSIDE the < > usually seen in the mail header, so this method
-
# will return only what is inside.
-
#
-
# Example:
-
#
-
# mail.message_id = '<1234@message.id>'
-
# mail.message_id #=> '1234@message.id'
-
#
-
# Also allows you to set the Message-ID by passing a string as a parameter
-
#
-
# mail.message_id '<1234@message.id>'
-
# mail.message_id #=> '1234@message.id'
-
1
def message_id( val = nil )
-
default :message_id, val
-
end
-
-
# Sets the Message-ID. Note, per RFC 2822 the Message ID consists of what is INSIDE
-
# the < > usually seen in the mail header, so this method will return only what is inside.
-
#
-
# mail.message_id = '<1234@message.id>'
-
# mail.message_id #=> '1234@message.id'
-
1
def message_id=( val )
-
header[:message_id] = val
-
end
-
-
# Returns the MIME version of the email as a string
-
#
-
# Example:
-
#
-
# mail.mime_version = '1.0'
-
# mail.mime_version #=> '1.0'
-
#
-
# Also allows you to set the MIME version by passing a string as a parameter.
-
#
-
# Example:
-
#
-
# mail.mime_version '1.0'
-
# mail.mime_version #=> '1.0'
-
1
def mime_version( val = nil )
-
default :mime_version, val
-
end
-
-
# Sets the MIME version of the email by accepting a string
-
#
-
# Example:
-
#
-
# mail.mime_version = '1.0'
-
# mail.mime_version #=> '1.0'
-
1
def mime_version=( val )
-
header[:mime_version] = val
-
end
-
-
1
def received( val = nil )
-
if val
-
header[:received] = val
-
else
-
header[:received]
-
end
-
end
-
-
1
def received=( val )
-
header[:received] = val
-
end
-
-
1
def references( val = nil )
-
default :references, val
-
end
-
-
1
def references=( val )
-
header[:references] = val
-
end
-
-
# Returns the Reply-To value of the mail object as an array of strings of
-
# address specs.
-
#
-
# Example:
-
#
-
# mail.reply_to = 'Mikel <mikel@test.lindsaar.net>'
-
# mail.reply_to #=> ['mikel@test.lindsaar.net']
-
# mail.reply_to = 'Mikel <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail.reply_to #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
#
-
# Also allows you to set the value by passing a value as a parameter
-
#
-
# Example:
-
#
-
# mail.reply_to 'Mikel <mikel@test.lindsaar.net>'
-
# mail.reply_to #=> ['mikel@test.lindsaar.net']
-
#
-
# Additionally, you can append new addresses to the returned Array like
-
# object.
-
#
-
# Example:
-
#
-
# mail.reply_to 'Mikel <mikel@test.lindsaar.net>'
-
# mail.reply_to << 'ada@test.lindsaar.net'
-
# mail.reply_to #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
1
def reply_to( val = nil )
-
default :reply_to, val
-
end
-
-
# Sets the Reply-To value of the mail object, pass in a string of the field
-
#
-
# Example:
-
#
-
# mail.reply_to = 'Mikel <mikel@test.lindsaar.net>'
-
# mail.reply_to #=> ['mikel@test.lindsaar.net']
-
# mail.reply_to = 'Mikel <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail.reply_to #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
1
def reply_to=( val )
-
header[:reply_to] = val
-
end
-
-
# Returns the Resent-Bcc value of the mail object as an array of strings of
-
# address specs.
-
#
-
# Example:
-
#
-
# mail.resent_bcc = 'Mikel <mikel@test.lindsaar.net>'
-
# mail.resent_bcc #=> ['mikel@test.lindsaar.net']
-
# mail.resent_bcc = 'Mikel <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail.resent_bcc #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
#
-
# Also allows you to set the value by passing a value as a parameter
-
#
-
# Example:
-
#
-
# mail.resent_bcc 'Mikel <mikel@test.lindsaar.net>'
-
# mail.resent_bcc #=> ['mikel@test.lindsaar.net']
-
#
-
# Additionally, you can append new addresses to the returned Array like
-
# object.
-
#
-
# Example:
-
#
-
# mail.resent_bcc 'Mikel <mikel@test.lindsaar.net>'
-
# mail.resent_bcc << 'ada@test.lindsaar.net'
-
# mail.resent_bcc #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
1
def resent_bcc( val = nil )
-
default :resent_bcc, val
-
end
-
-
# Sets the Resent-Bcc value of the mail object, pass in a string of the field
-
#
-
# Example:
-
#
-
# mail.resent_bcc = 'Mikel <mikel@test.lindsaar.net>'
-
# mail.resent_bcc #=> ['mikel@test.lindsaar.net']
-
# mail.resent_bcc = 'Mikel <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail.resent_bcc #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
1
def resent_bcc=( val )
-
header[:resent_bcc] = val
-
end
-
-
# Returns the Resent-Cc value of the mail object as an array of strings of
-
# address specs.
-
#
-
# Example:
-
#
-
# mail.resent_cc = 'Mikel <mikel@test.lindsaar.net>'
-
# mail.resent_cc #=> ['mikel@test.lindsaar.net']
-
# mail.resent_cc = 'Mikel <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail.resent_cc #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
#
-
# Also allows you to set the value by passing a value as a parameter
-
#
-
# Example:
-
#
-
# mail.resent_cc 'Mikel <mikel@test.lindsaar.net>'
-
# mail.resent_cc #=> ['mikel@test.lindsaar.net']
-
#
-
# Additionally, you can append new addresses to the returned Array like
-
# object.
-
#
-
# Example:
-
#
-
# mail.resent_cc 'Mikel <mikel@test.lindsaar.net>'
-
# mail.resent_cc << 'ada@test.lindsaar.net'
-
# mail.resent_cc #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
1
def resent_cc( val = nil )
-
default :resent_cc, val
-
end
-
-
# Sets the Resent-Cc value of the mail object, pass in a string of the field
-
#
-
# Example:
-
#
-
# mail.resent_cc = 'Mikel <mikel@test.lindsaar.net>'
-
# mail.resent_cc #=> ['mikel@test.lindsaar.net']
-
# mail.resent_cc = 'Mikel <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail.resent_cc #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
1
def resent_cc=( val )
-
header[:resent_cc] = val
-
end
-
-
1
def resent_date( val = nil )
-
default :resent_date, val
-
end
-
-
1
def resent_date=( val )
-
header[:resent_date] = val
-
end
-
-
# Returns the Resent-From value of the mail object as an array of strings of
-
# address specs.
-
#
-
# Example:
-
#
-
# mail.resent_from = 'Mikel <mikel@test.lindsaar.net>'
-
# mail.resent_from #=> ['mikel@test.lindsaar.net']
-
# mail.resent_from = 'Mikel <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail.resent_from #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
#
-
# Also allows you to set the value by passing a value as a parameter
-
#
-
# Example:
-
#
-
# mail.resent_from ['Mikel <mikel@test.lindsaar.net>']
-
# mail.resent_from #=> 'mikel@test.lindsaar.net'
-
#
-
# Additionally, you can append new addresses to the returned Array like
-
# object.
-
#
-
# Example:
-
#
-
# mail.resent_from 'Mikel <mikel@test.lindsaar.net>'
-
# mail.resent_from << 'ada@test.lindsaar.net'
-
# mail.resent_from #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
1
def resent_from( val = nil )
-
default :resent_from, val
-
end
-
-
# Sets the Resent-From value of the mail object, pass in a string of the field
-
#
-
# Example:
-
#
-
# mail.resent_from = 'Mikel <mikel@test.lindsaar.net>'
-
# mail.resent_from #=> ['mikel@test.lindsaar.net']
-
# mail.resent_from = 'Mikel <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail.resent_from #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
1
def resent_from=( val )
-
header[:resent_from] = val
-
end
-
-
1
def resent_message_id( val = nil )
-
default :resent_message_id, val
-
end
-
-
1
def resent_message_id=( val )
-
header[:resent_message_id] = val
-
end
-
-
# Returns the Resent-Sender value of the mail object, as a single string of an address
-
# spec. A sender per RFC 2822 must be a single address, so you can not append to
-
# this address.
-
#
-
# Example:
-
#
-
# mail.resent_sender = 'Mikel <mikel@test.lindsaar.net>'
-
# mail.resent_sender #=> 'mikel@test.lindsaar.net'
-
#
-
# Also allows you to set the value by passing a value as a parameter
-
#
-
# Example:
-
#
-
# mail.resent_sender 'Mikel <mikel@test.lindsaar.net>'
-
# mail.resent_sender #=> 'mikel@test.lindsaar.net'
-
1
def resent_sender( val = nil )
-
default :resent_sender, val
-
end
-
-
# Sets the Resent-Sender value of the mail object, pass in a string of the field
-
#
-
# Example:
-
#
-
# mail.resent_sender = 'Mikel <mikel@test.lindsaar.net>'
-
# mail.resent_sender #=> 'mikel@test.lindsaar.net'
-
1
def resent_sender=( val )
-
header[:resent_sender] = val
-
end
-
-
# Returns the Resent-To value of the mail object as an array of strings of
-
# address specs.
-
#
-
# Example:
-
#
-
# mail.resent_to = 'Mikel <mikel@test.lindsaar.net>'
-
# mail.resent_to #=> ['mikel@test.lindsaar.net']
-
# mail.resent_to = 'Mikel <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail.resent_to #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
#
-
# Also allows you to set the value by passing a value as a parameter
-
#
-
# Example:
-
#
-
# mail.resent_to 'Mikel <mikel@test.lindsaar.net>'
-
# mail.resent_to #=> ['mikel@test.lindsaar.net']
-
#
-
# Additionally, you can append new addresses to the returned Array like
-
# object.
-
#
-
# Example:
-
#
-
# mail.resent_to 'Mikel <mikel@test.lindsaar.net>'
-
# mail.resent_to << 'ada@test.lindsaar.net'
-
# mail.resent_to #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
1
def resent_to( val = nil )
-
default :resent_to, val
-
end
-
-
# Sets the Resent-To value of the mail object, pass in a string of the field
-
#
-
# Example:
-
#
-
# mail.resent_to = 'Mikel <mikel@test.lindsaar.net>'
-
# mail.resent_to #=> ['mikel@test.lindsaar.net']
-
# mail.resent_to = 'Mikel <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail.resent_to #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
1
def resent_to=( val )
-
header[:resent_to] = val
-
end
-
-
# Returns the return path of the mail object, or sets it if you pass a string
-
1
def return_path( val = nil )
-
default :return_path, val
-
end
-
-
# Sets the return path of the object
-
1
def return_path=( val )
-
header[:return_path] = val
-
end
-
-
# Returns the Sender value of the mail object, as a single string of an address
-
# spec. A sender per RFC 2822 must be a single address.
-
#
-
# Example:
-
#
-
# mail.sender = 'Mikel <mikel@test.lindsaar.net>'
-
# mail.sender #=> 'mikel@test.lindsaar.net'
-
#
-
# Also allows you to set the value by passing a value as a parameter
-
#
-
# Example:
-
#
-
# mail.sender 'Mikel <mikel@test.lindsaar.net>'
-
# mail.sender #=> 'mikel@test.lindsaar.net'
-
1
def sender( val = nil )
-
default :sender, val
-
end
-
-
# Sets the Sender value of the mail object, pass in a string of the field
-
#
-
# Example:
-
#
-
# mail.sender = 'Mikel <mikel@test.lindsaar.net>'
-
# mail.sender #=> 'mikel@test.lindsaar.net'
-
1
def sender=( val )
-
header[:sender] = val
-
end
-
-
# Returns the SMTP Envelope From value of the mail object, as a single
-
# string of an address spec.
-
#
-
# Defaults to Return-Path, Sender, or the first From address.
-
#
-
# Example:
-
#
-
# mail.smtp_envelope_from = 'Mikel <mikel@test.lindsaar.net>'
-
# mail.smtp_envelope_from #=> 'mikel@test.lindsaar.net'
-
#
-
# Also allows you to set the value by passing a value as a parameter
-
#
-
# Example:
-
#
-
# mail.smtp_envelope_from 'Mikel <mikel@test.lindsaar.net>'
-
# mail.smtp_envelope_from #=> 'mikel@test.lindsaar.net'
-
1
def smtp_envelope_from( val = nil )
-
if val
-
self.smtp_envelope_from = val
-
else
-
@smtp_envelope_from || return_path || sender || from_addrs.first
-
end
-
end
-
-
# Sets the From address on the SMTP Envelope.
-
#
-
# Example:
-
#
-
# mail.smtp_envelope_from = 'Mikel <mikel@test.lindsaar.net>'
-
# mail.smtp_envelope_from #=> 'mikel@test.lindsaar.net'
-
1
def smtp_envelope_from=( val )
-
@smtp_envelope_from = val
-
end
-
-
# Returns the SMTP Envelope To value of the mail object.
-
#
-
# Defaults to #destinations: To, Cc, and Bcc addresses.
-
#
-
# Example:
-
#
-
# mail.smtp_envelope_to = 'Mikel <mikel@test.lindsaar.net>'
-
# mail.smtp_envelope_to #=> 'mikel@test.lindsaar.net'
-
#
-
# Also allows you to set the value by passing a value as a parameter
-
#
-
# Example:
-
#
-
# mail.smtp_envelope_to ['Mikel <mikel@test.lindsaar.net>', 'Lindsaar <lindsaar@test.lindsaar.net>']
-
# mail.smtp_envelope_to #=> ['mikel@test.lindsaar.net', 'lindsaar@test.lindsaar.net']
-
1
def smtp_envelope_to( val = nil )
-
if val
-
self.smtp_envelope_to = val
-
else
-
@smtp_envelope_to || destinations
-
end
-
end
-
-
# Sets the To addresses on the SMTP Envelope.
-
#
-
# Example:
-
#
-
# mail.smtp_envelope_to = 'Mikel <mikel@test.lindsaar.net>'
-
# mail.smtp_envelope_to #=> 'mikel@test.lindsaar.net'
-
#
-
# mail.smtp_envelope_to = ['Mikel <mikel@test.lindsaar.net>', 'Lindsaar <lindsaar@test.lindsaar.net>']
-
# mail.smtp_envelope_to #=> ['mikel@test.lindsaar.net', 'lindsaar@test.lindsaar.net']
-
1
def smtp_envelope_to=( val )
-
@smtp_envelope_to =
-
case val
-
when Array, NilClass
-
val
-
else
-
[val]
-
end
-
end
-
-
# Returns the decoded value of the subject field, as a single string.
-
#
-
# Example:
-
#
-
# mail.subject = "G'Day mate"
-
# mail.subject #=> "G'Day mate"
-
# mail.subject = '=?UTF-8?Q?This_is_=E3=81=82_string?='
-
# mail.subject #=> "This is ��� string"
-
#
-
# Also allows you to set the value by passing a value as a parameter
-
#
-
# Example:
-
#
-
# mail.subject "G'Day mate"
-
# mail.subject #=> "G'Day mate"
-
1
def subject( val = nil )
-
default :subject, val
-
end
-
-
# Sets the Subject value of the mail object, pass in a string of the field
-
#
-
# Example:
-
#
-
# mail.subject = '=?UTF-8?Q?This_is_=E3=81=82_string?='
-
# mail.subject #=> "This is ��� string"
-
1
def subject=( val )
-
header[:subject] = val
-
end
-
-
# Returns the To value of the mail object as an array of strings of
-
# address specs.
-
#
-
# Example:
-
#
-
# mail.to = 'Mikel <mikel@test.lindsaar.net>'
-
# mail.to #=> ['mikel@test.lindsaar.net']
-
# mail.to = 'Mikel <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail.to #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
#
-
# Also allows you to set the value by passing a value as a parameter
-
#
-
# Example:
-
#
-
# mail.to 'Mikel <mikel@test.lindsaar.net>'
-
# mail.to #=> ['mikel@test.lindsaar.net']
-
#
-
# Additionally, you can append new addresses to the returned Array like
-
# object.
-
#
-
# Example:
-
#
-
# mail.to 'Mikel <mikel@test.lindsaar.net>'
-
# mail.to << 'ada@test.lindsaar.net'
-
# mail.to #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
1
def to( val = nil )
-
default :to, val
-
end
-
-
# Sets the To value of the mail object, pass in a string of the field
-
#
-
# Example:
-
#
-
# mail.to = 'Mikel <mikel@test.lindsaar.net>'
-
# mail.to #=> ['mikel@test.lindsaar.net']
-
# mail.to = 'Mikel <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail.to #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
1
def to=( val )
-
header[:to] = val
-
end
-
-
# Returns the default value of the field requested as a symbol.
-
#
-
# Each header field has a :default method which returns the most common use case for
-
# that field, for example, the date field types will return a DateTime object when
-
# sent :default, the subject, or unstructured fields will return a decoded string of
-
# their value, the address field types will return a single addr_spec or an array of
-
# addr_specs if there is more than one.
-
1
def default( sym, val = nil )
-
if val
-
header[sym] = val
-
elsif field = header[sym]
-
field.default
-
end
-
end
-
-
# Sets the body object of the message object.
-
#
-
# Example:
-
#
-
# mail.body = 'This is the body'
-
# mail.body #=> #<Mail::Body:0x13919c @raw_source="This is the bo...
-
#
-
# You can also reset the body of an Message object by setting body to nil
-
#
-
# Example:
-
#
-
# mail.body = 'this is the body'
-
# mail.body.encoded #=> 'this is the body'
-
# mail.body = nil
-
# mail.body.encoded #=> ''
-
#
-
# If you try and set the body of an email that is a multipart email, then instead
-
# of deleting all the parts of your email, mail will add a text/plain part to
-
# your email:
-
#
-
# mail.add_file 'somefilename.png'
-
# mail.parts.length #=> 1
-
# mail.body = "This is a body"
-
# mail.parts.length #=> 2
-
# mail.parts.last.content_type.content_type #=> 'This is a body'
-
1
def body=(value)
-
body_lazy(value)
-
end
-
-
# Returns the body of the message object. Or, if passed
-
# a parameter sets the value.
-
#
-
# Example:
-
#
-
# mail = Mail::Message.new('To: mikel\r\n\r\nThis is the body')
-
# mail.body #=> #<Mail::Body:0x13919c @raw_source="This is the bo...
-
#
-
# mail.body 'This is another body'
-
# mail.body #=> #<Mail::Body:0x13919c @raw_source="This is anothe...
-
1
def body(value = nil)
-
if value
-
self.body = value
-
else
-
process_body_raw if @body_raw
-
@body
-
end
-
end
-
-
1
def body_encoding(value = nil)
-
if value.nil?
-
body.encoding
-
else
-
body.encoding = value
-
end
-
end
-
-
1
def body_encoding=(value)
-
body.encoding = value
-
end
-
-
# Returns the list of addresses this message should be sent to by
-
# collecting the addresses off the to, cc and bcc fields.
-
#
-
# Example:
-
#
-
# mail.to = 'mikel@test.lindsaar.net'
-
# mail.cc = 'sam@test.lindsaar.net'
-
# mail.bcc = 'bob@test.lindsaar.net'
-
# mail.destinations.length #=> 3
-
# mail.destinations.first #=> 'mikel@test.lindsaar.net'
-
1
def destinations
-
[to_addrs, cc_addrs, bcc_addrs].compact.flatten
-
end
-
-
# Returns an array of addresses (the encoded value) in the From field,
-
# if no From field, returns an empty array
-
1
def from_addrs
-
from ? [from].flatten : []
-
end
-
-
# Returns an array of addresses (the encoded value) in the To field,
-
# if no To field, returns an empty array
-
1
def to_addrs
-
to ? [to].flatten : []
-
end
-
-
# Returns an array of addresses (the encoded value) in the Cc field,
-
# if no Cc field, returns an empty array
-
1
def cc_addrs
-
cc ? [cc].flatten : []
-
end
-
-
# Returns an array of addresses (the encoded value) in the Bcc field,
-
# if no Bcc field, returns an empty array
-
1
def bcc_addrs
-
bcc ? [bcc].flatten : []
-
end
-
-
# Allows you to add an arbitrary header
-
#
-
# Example:
-
#
-
# mail['foo'] = '1234'
-
# mail['foo'].to_s #=> '1234'
-
1
def []=(name, value)
-
if name.to_s == 'body'
-
self.body = value
-
elsif name.to_s =~ /content[-_]type/i
-
header[name] = value
-
elsif name.to_s == 'charset'
-
self.charset = value
-
else
-
header[name] = value
-
end
-
end
-
-
# Allows you to read an arbitrary header
-
#
-
# Example:
-
#
-
# mail['foo'] = '1234'
-
# mail['foo'].to_s #=> '1234'
-
1
def [](name)
-
header[underscoreize(name)]
-
end
-
-
# Method Missing in this implementation allows you to set any of the
-
# standard fields directly as you would the "to", "subject" etc.
-
#
-
# Those fields used most often (to, subject et al) are given their
-
# own method for ease of documentation and also to avoid the hook
-
# call to method missing.
-
#
-
# This will only catch the known fields listed in:
-
#
-
# Mail::Field::KNOWN_FIELDS
-
#
-
# as per RFC 2822, any ruby string or method name could pretty much
-
# be a field name, so we don't want to just catch ANYTHING sent to
-
# a message object and interpret it as a header.
-
#
-
# This method provides all three types of header call to set, read
-
# and explicitly set with the = operator
-
#
-
# Examples:
-
#
-
# mail.comments = 'These are some comments'
-
# mail.comments #=> 'These are some comments'
-
#
-
# mail.comments 'These are other comments'
-
# mail.comments #=> 'These are other comments'
-
#
-
#
-
# mail.date = 'Tue, 1 Jul 2003 10:52:37 +0200'
-
# mail.date.to_s #=> 'Tue, 1 Jul 2003 10:52:37 +0200'
-
#
-
# mail.date 'Tue, 1 Jul 2003 10:52:37 +0200'
-
# mail.date.to_s #=> 'Tue, 1 Jul 2003 10:52:37 +0200'
-
#
-
#
-
# mail.resent_msg_id = '<1234@resent_msg_id.lindsaar.net>'
-
# mail.resent_msg_id #=> '<1234@resent_msg_id.lindsaar.net>'
-
#
-
# mail.resent_msg_id '<4567@resent_msg_id.lindsaar.net>'
-
# mail.resent_msg_id #=> '<4567@resent_msg_id.lindsaar.net>'
-
1
def method_missing(name, *args, &block)
-
#:nodoc:
-
# Only take the structured fields, as we could take _anything_ really
-
# as it could become an optional field... "but therin lies the dark side"
-
field_name = underscoreize(name).chomp("=")
-
if Mail::Field::KNOWN_FIELDS.include?(field_name)
-
if args.empty?
-
header[field_name]
-
else
-
header[field_name] = args.first
-
end
-
else
-
super # otherwise pass it on
-
end
-
#:startdoc:
-
end
-
-
# Returns an FieldList of all the fields in the header in the order that
-
# they appear in the header
-
1
def header_fields
-
header.fields
-
end
-
-
# Returns true if the message has a message ID field, the field may or may
-
# not have a value, but the field exists or not.
-
1
def has_message_id?
-
header.has_message_id?
-
end
-
-
# Returns true if the message has a Date field, the field may or may
-
# not have a value, but the field exists or not.
-
1
def has_date?
-
header.has_date?
-
end
-
-
# Returns true if the message has a Mime-Version field, the field may or may
-
# not have a value, but the field exists or not.
-
1
def has_mime_version?
-
header.has_mime_version?
-
end
-
-
1
def has_content_type?
-
tmp = header[:content_type].main_type rescue nil
-
!!tmp
-
end
-
-
1
def has_charset?
-
tmp = header[:content_type].parameters rescue nil
-
!!(has_content_type? && tmp && tmp['charset'])
-
end
-
-
1
def has_content_transfer_encoding?
-
header[:content_transfer_encoding] && Utilities.blank?(header[:content_transfer_encoding].errors)
-
end
-
-
1
def has_transfer_encoding? # :nodoc:
-
warn(":has_transfer_encoding? is deprecated in Mail 1.4.3. Please use has_content_transfer_encoding?\n#{caller}")
-
has_content_transfer_encoding?
-
end
-
-
# Creates a new empty Message-ID field and inserts it in the correct order
-
# into the Header. The MessageIdField object will automatically generate
-
# a unique message ID if you try and encode it or output it to_s without
-
# specifying a message id.
-
#
-
# It will preserve the message ID you specify if you do.
-
1
def add_message_id(msg_id_val = '')
-
header['message-id'] = msg_id_val
-
end
-
-
# Creates a new empty Date field and inserts it in the correct order
-
# into the Header. The DateField object will automatically generate
-
# DateTime.now's date if you try and encode it or output it to_s without
-
# specifying a date yourself.
-
#
-
# It will preserve any date you specify if you do.
-
1
def add_date(date_val = '')
-
header['date'] = date_val
-
end
-
-
# Creates a new empty Mime Version field and inserts it in the correct order
-
# into the Header. The MimeVersion object will automatically generate
-
# set itself to '1.0' if you try and encode it or output it to_s without
-
# specifying a version yourself.
-
#
-
# It will preserve any date you specify if you do.
-
1
def add_mime_version(ver_val = '')
-
header['mime-version'] = ver_val
-
end
-
-
# Adds a content type and charset if the body is US-ASCII
-
#
-
# Otherwise raises a warning
-
1
def add_content_type
-
header[:content_type] = 'text/plain'
-
end
-
-
# Adds a content type and charset if the body is US-ASCII
-
#
-
# Otherwise raises a warning
-
1
def add_charset
-
if !body.empty?
-
# Only give a warning if this isn't an attachment, has non US-ASCII and the user
-
# has not specified an encoding explicitly.
-
if @defaulted_charset && !body.raw_source.ascii_only? && !self.attachment?
-
warning = "Non US-ASCII detected and no charset defined.\nDefaulting to UTF-8, set your own if this is incorrect.\n"
-
warn(warning)
-
end
-
header[:content_type].parameters['charset'] = @charset
-
end
-
end
-
-
# Adds a content transfer encoding
-
1
def add_content_transfer_encoding
-
header[:content_transfer_encoding] ||= body.default_encoding
-
end
-
-
1
def add_transfer_encoding # :nodoc:
-
warn(":add_transfer_encoding is deprecated in Mail 1.4.3. Please use add_content_transfer_encoding\n#{caller}")
-
add_content_transfer_encoding
-
end
-
-
1
def transfer_encoding # :nodoc:
-
warn(":transfer_encoding is deprecated in Mail 1.4.3. Please use content_transfer_encoding\n#{caller}")
-
content_transfer_encoding
-
end
-
-
# Returns the MIME media type of part we are on, this is taken from the content-type header
-
1
def mime_type
-
has_content_type? ? header[:content_type].string : nil rescue nil
-
end
-
-
1
def message_content_type
-
warn(":message_content_type is deprecated in Mail 1.4.3. Please use mime_type\n#{caller}")
-
mime_type
-
end
-
-
# Returns the character set defined in the content type field
-
1
def charset
-
if @header
-
has_content_type? ? content_type_parameters['charset'] : @charset
-
else
-
@charset
-
end
-
end
-
-
# Sets the charset to the supplied value.
-
1
def charset=(value)
-
@defaulted_charset = false
-
@charset = value
-
@header.charset = value
-
end
-
-
# Returns the main content type
-
1
def main_type
-
has_content_type? ? header[:content_type].main_type : nil rescue nil
-
end
-
-
# Returns the sub content type
-
1
def sub_type
-
has_content_type? ? header[:content_type].sub_type : nil rescue nil
-
end
-
-
# Returns the content type parameters
-
1
def mime_parameters
-
warn(':mime_parameters is deprecated in Mail 1.4.3, please use :content_type_parameters instead')
-
content_type_parameters
-
end
-
-
# Returns the content type parameters
-
1
def content_type_parameters
-
has_content_type? ? header[:content_type].parameters : nil rescue nil
-
end
-
-
# Returns true if the message is multipart
-
1
def multipart?
-
has_content_type? ? !!(main_type =~ /^multipart$/i) : false
-
end
-
-
# Returns true if the message is a multipart/report
-
1
def multipart_report?
-
multipart? && sub_type =~ /^report$/i
-
end
-
-
# Returns true if the message is a multipart/report; report-type=delivery-status;
-
1
def delivery_status_report?
-
multipart_report? && content_type_parameters['report-type'] =~ /^delivery-status$/i
-
end
-
-
# returns the part in a multipart/report email that has the content-type delivery-status
-
1
def delivery_status_part
-
unless defined? @delivery_status_part
-
@delivery_status_part =
-
if delivery_status_report?
-
parts.detect(&:delivery_status_report_part?)
-
end
-
end
-
-
@delivery_status_part
-
end
-
-
1
def bounced?
-
delivery_status_part and delivery_status_part.bounced?
-
end
-
-
1
def action
-
delivery_status_part and delivery_status_part.action
-
end
-
-
1
def final_recipient
-
delivery_status_part and delivery_status_part.final_recipient
-
end
-
-
1
def error_status
-
delivery_status_part and delivery_status_part.error_status
-
end
-
-
1
def diagnostic_code
-
delivery_status_part and delivery_status_part.diagnostic_code
-
end
-
-
1
def remote_mta
-
delivery_status_part and delivery_status_part.remote_mta
-
end
-
-
1
def retryable?
-
delivery_status_part and delivery_status_part.retryable?
-
end
-
-
# Returns the current boundary for this message part
-
1
def boundary
-
content_type_parameters ? content_type_parameters['boundary'] : nil
-
end
-
-
# Returns a parts list object of all the parts in the message
-
1
def parts
-
body.parts
-
end
-
-
# Returns an AttachmentsList object, which holds all of the attachments in
-
# the receiver object (either the entire email or a part within) and all
-
# of its descendants.
-
#
-
# It also allows you to add attachments to the mail object directly, like so:
-
#
-
# mail.attachments['filename.jpg'] = File.read('/path/to/filename.jpg')
-
#
-
# If you do this, then Mail will take the file name and work out the MIME media type
-
# set the Content-Type, Content-Disposition, Content-Transfer-Encoding and
-
# base64 encode the contents of the attachment all for you.
-
#
-
# You can also specify overrides if you want by passing a hash instead of a string:
-
#
-
# mail.attachments['filename.jpg'] = {:mime_type => 'application/x-gzip',
-
# :content => File.read('/path/to/filename.jpg')}
-
#
-
# If you want to use a different encoding than Base64, you can pass an encoding in,
-
# but then it is up to you to pass in the content pre-encoded, and don't expect
-
# Mail to know how to decode this data:
-
#
-
# file_content = SpecialEncode(File.read('/path/to/filename.jpg'))
-
# mail.attachments['filename.jpg'] = {:mime_type => 'application/x-gzip',
-
# :encoding => 'SpecialEncoding',
-
# :content => file_content }
-
#
-
# You can also search for specific attachments:
-
#
-
# # By Filename
-
# mail.attachments['filename.jpg'] #=> Mail::Part object or nil
-
#
-
# # or by index
-
# mail.attachments[0] #=> Mail::Part (first attachment)
-
#
-
1
def attachments
-
parts.attachments
-
end
-
-
1
def has_attachments?
-
!attachments.empty?
-
end
-
-
# Accessor for html_part
-
1
def html_part(&block)
-
if block_given?
-
self.html_part = Mail::Part.new(:content_type => 'text/html', &block)
-
else
-
@html_part || find_first_mime_type('text/html')
-
end
-
end
-
-
# Accessor for text_part
-
1
def text_part(&block)
-
if block_given?
-
self.text_part = Mail::Part.new(:content_type => 'text/plain', &block)
-
else
-
@text_part || find_first_mime_type('text/plain')
-
end
-
end
-
-
# Helper to add a html part to a multipart/alternative email. If this and
-
# text_part are both defined in a message, then it will be a multipart/alternative
-
# message and set itself that way.
-
1
def html_part=(msg)
-
# Assign the html part and set multipart/alternative if there's a text part.
-
if msg
-
msg = Mail::Part.new(:body => msg) unless msg.kind_of?(Mail::Message)
-
-
@html_part = msg
-
@html_part.content_type = 'text/html' unless @html_part.has_content_type?
-
add_multipart_alternate_header if text_part
-
add_part @html_part
-
-
# If nil, delete the html part and back out of multipart/alternative.
-
elsif @html_part
-
parts.delete_if { |p| p.object_id == @html_part.object_id }
-
@html_part = nil
-
if text_part
-
self.content_type = nil
-
body.boundary = nil
-
end
-
end
-
end
-
-
# Helper to add a text part to a multipart/alternative email. If this and
-
# html_part are both defined in a message, then it will be a multipart/alternative
-
# message and set itself that way.
-
1
def text_part=(msg)
-
# Assign the text part and set multipart/alternative if there's an html part.
-
if msg
-
msg = Mail::Part.new(:body => msg) unless msg.kind_of?(Mail::Message)
-
-
@text_part = msg
-
@text_part.content_type = 'text/plain' unless @text_part.has_content_type?
-
add_multipart_alternate_header if html_part
-
add_part @text_part
-
-
# If nil, delete the text part and back out of multipart/alternative.
-
elsif @text_part
-
parts.delete_if { |p| p.object_id == @text_part.object_id }
-
@text_part = nil
-
if html_part
-
self.content_type = nil
-
body.boundary = nil
-
end
-
end
-
end
-
-
# Adds a part to the parts list or creates the part list
-
1
def add_part(part)
-
if !body.multipart? && !Utilities.blank?(self.body.decoded)
-
@text_part = Mail::Part.new('Content-Type: text/plain;')
-
@text_part.body = body.decoded
-
self.body << @text_part
-
add_multipart_alternate_header
-
end
-
add_boundary
-
self.body << part
-
end
-
-
# Allows you to add a part in block form to an existing mail message object
-
#
-
# Example:
-
#
-
# mail = Mail.new do
-
# part :content_type => "multipart/alternative", :content_disposition => "inline" do |p|
-
# p.part :content_type => "text/plain", :body => "test text\nline #2"
-
# p.part :content_type => "text/html", :body => "<b>test</b> HTML<br/>\nline #2"
-
# end
-
# end
-
1
def part(params = {})
-
new_part = Part.new(params)
-
yield new_part if block_given?
-
add_part(new_part)
-
end
-
-
# Adds a file to the message. You have two options with this method, you can
-
# just pass in the absolute path to the file you want and Mail will read the file,
-
# get the filename from the path you pass in and guess the MIME media type, or you
-
# can pass in the filename as a string, and pass in the file content as a blob.
-
#
-
# Example:
-
#
-
# m = Mail.new
-
# m.add_file('/path/to/filename.png')
-
#
-
# m = Mail.new
-
# m.add_file(:filename => 'filename.png', :content => File.read('/path/to/file.jpg'))
-
#
-
# Note also that if you add a file to an existing message, Mail will convert that message
-
# to a MIME multipart email, moving whatever plain text body you had into its own text
-
# plain part.
-
#
-
# Example:
-
#
-
# m = Mail.new do
-
# body 'this is some text'
-
# end
-
# m.multipart? #=> false
-
# m.add_file('/path/to/filename.png')
-
# m.multipart? #=> true
-
# m.parts.first.content_type.content_type #=> 'text/plain'
-
# m.parts.last.content_type.content_type #=> 'image/png'
-
#
-
# See also #attachments
-
1
def add_file(values)
-
convert_to_multipart unless self.multipart? || Utilities.blank?(self.body.decoded)
-
add_multipart_mixed_header
-
if values.is_a?(String)
-
basename = File.basename(values)
-
filedata = File.open(values, 'rb') { |f| f.read }
-
else
-
basename = values[:filename]
-
filedata = values
-
end
-
self.attachments[basename] = filedata
-
end
-
-
1
def convert_to_multipart
-
text = body.decoded
-
self.body = ''
-
text_part = Mail::Part.new({:content_type => 'text/plain;',
-
:body => text})
-
text_part.charset = charset unless @defaulted_charset
-
self.body << text_part
-
end
-
-
# Encodes the message, calls encode on all its parts, gets an email message
-
# ready to send
-
1
def ready_to_send!
-
identify_and_set_transfer_encoding
-
parts.each do |part|
-
part.transport_encoding = transport_encoding
-
part.ready_to_send!
-
end
-
add_required_fields
-
end
-
-
1
def encode!
-
warn("Deprecated in 1.1.0 in favour of :ready_to_send! as it is less confusing with encoding and decoding.")
-
ready_to_send!
-
end
-
-
# Outputs an encoded string representation of the mail message including
-
# all headers, attachments, etc. This is an encoded email in US-ASCII,
-
# so it is able to be directly sent to an email server.
-
1
def encoded
-
ready_to_send!
-
buffer = header.encoded
-
buffer << "\r\n"
-
buffer << body.encoded(content_transfer_encoding)
-
buffer
-
end
-
-
1
def without_attachments!
-
if has_attachments?
-
parts.delete_if { |p| p.attachment? }
-
-
reencoded = parts.empty? ? '' : body.encoded(content_transfer_encoding)
-
@body = nil # So the new parts won't be added to the existing body
-
self.body = reencoded
-
end
-
-
self
-
end
-
-
1
def to_yaml(opts = {})
-
hash = {}
-
hash['headers'] = {}
-
header.fields.each do |field|
-
hash['headers'][field.name] = field.value
-
end
-
hash['delivery_handler'] = delivery_handler.to_s if delivery_handler
-
hash['transport_encoding'] = transport_encoding.to_s
-
special_variables = [:@header, :@delivery_handler, :@transport_encoding]
-
if multipart?
-
hash['multipart_body'] = []
-
body.parts.map { |part| hash['multipart_body'] << part.to_yaml }
-
special_variables.push(:@body, :@text_part, :@html_part)
-
end
-
(instance_variables.map(&:to_sym) - special_variables).each do |var|
-
hash[var.to_s] = instance_variable_get(var)
-
end
-
hash.to_yaml(opts)
-
end
-
-
1
def self.from_yaml(str)
-
hash = YAML.load(str)
-
m = self.new(:headers => hash['headers'])
-
hash.delete('headers')
-
hash.each do |k,v|
-
case
-
when k == 'delivery_handler'
-
begin
-
m.delivery_handler = Object.const_get(v) unless Utilities.blank?(v)
-
rescue NameError
-
end
-
when k == 'transport_encoding'
-
m.transport_encoding(v)
-
when k == 'multipart_body'
-
v.map {|part| m.add_part Mail::Part.from_yaml(part) }
-
when k =~ /^@/
-
m.instance_variable_set(k.to_sym, v)
-
end
-
end
-
m
-
end
-
-
1
def self.from_hash(hash)
-
Mail::Message.new(hash)
-
end
-
-
1
def to_s
-
encoded
-
end
-
-
1
def inspect
-
"#<#{self.class}:#{self.object_id}, Multipart: #{multipart?}, Headers: #{header.field_summary}>"
-
end
-
-
1
def decoded
-
case
-
when self.text?
-
decode_body_as_text
-
when self.attachment?
-
decode_body
-
when !self.multipart?
-
body.decoded
-
else
-
raise NoMethodError, 'Can not decode an entire message, try calling #decoded on the various fields and body or parts if it is a multipart message.'
-
end
-
end
-
-
1
def read
-
if self.attachment?
-
decode_body
-
else
-
raise NoMethodError, 'Can not call read on a part unless it is an attachment.'
-
end
-
end
-
-
1
def decode_body
-
body.decoded
-
end
-
-
# Returns true if this part is an attachment,
-
# false otherwise.
-
1
def attachment?
-
!!find_attachment
-
end
-
-
# Returns the attachment data if there is any
-
1
def attachment
-
@attachment
-
end
-
-
# Returns the filename of the attachment
-
1
def filename
-
find_attachment
-
end
-
-
1
def all_parts
-
parts.map { |p| [p, p.all_parts] }.flatten
-
end
-
-
1
def find_first_mime_type(mt)
-
all_parts.detect { |p| p.mime_type == mt && !p.attachment? }
-
end
-
-
# Skips the deletion of this message. All other messages
-
# flagged for delete still will be deleted at session close (i.e. when
-
# #find exits). Only has an effect if you're using #find_and_delete
-
# or #find with :delete_after_find set to true.
-
1
def skip_deletion
-
@mark_for_delete = false
-
end
-
-
# Sets whether this message should be deleted at session close (i.e.
-
# after #find). Message will only be deleted if messages are retrieved
-
# using the #find_and_delete method, or by calling #find with
-
# :delete_after_find set to true.
-
1
def mark_for_delete=(value = true)
-
@mark_for_delete = value
-
end
-
-
# Returns whether message will be marked for deletion.
-
# If so, the message will be deleted at session close (i.e. after #find
-
# exits), but only if also using the #find_and_delete method, or by
-
# calling #find with :delete_after_find set to true.
-
#
-
# Side-note: Just to be clear, this method will return true even if
-
# the message hasn't yet been marked for delete on the mail server.
-
# However, if this method returns true, it *will be* marked on the
-
# server after each block yields back to #find or #find_and_delete.
-
1
def is_marked_for_delete?
-
return @mark_for_delete
-
end
-
-
1
def text?
-
has_content_type? ? !!(main_type =~ /^text$/i) : false
-
end
-
-
1
private
-
-
1
HEADER_SEPARATOR = /#{Constants::CRLF}#{Constants::CRLF}/
-
-
# 2.1. General Description
-
# A message consists of header fields (collectively called "the header
-
# of the message") followed, optionally, by a body. The header is a
-
# sequence of lines of characters with special syntax as defined in
-
# this standard. The body is simply a sequence of characters that
-
# follows the header and is separated from the header by an empty line
-
# (i.e., a line with nothing preceding the CRLF).
-
1
def parse_message
-
header_part, body_part = raw_source.lstrip.split(HEADER_SEPARATOR, 2)
-
self.header = header_part
-
self.body = body_part
-
end
-
-
1
def raw_source=(value)
-
@raw_source = value
-
end
-
-
# see comments to body=. We take data and process it lazily
-
1
def body_lazy(value)
-
process_body_raw if @body_raw && value
-
case
-
when value == nil || value.length<=0
-
@body = Mail::Body.new('')
-
@body_raw = nil
-
add_encoding_to_body
-
when @body && @body.multipart?
-
self.text_part = value
-
else
-
@body_raw = value
-
end
-
end
-
-
-
1
def process_body_raw
-
@body = Mail::Body.new(@body_raw)
-
@body_raw = nil
-
separate_parts if @separate_parts
-
-
add_encoding_to_body
-
end
-
-
1
def set_envelope_header
-
raw_string = raw_source.to_s
-
if match_data = raw_string.match(/\AFrom\s(#{TEXT}+)#{Constants::CRLF}/m)
-
set_envelope(match_data[1])
-
self.raw_source = raw_string.sub(match_data[0], "")
-
end
-
end
-
-
1
def separate_parts
-
body.split!(boundary)
-
end
-
-
1
def allowed_encodings
-
case mime_type
-
when 'message/rfc822'
-
[Encodings::SevenBit, Encodings::EightBit, Encodings::Binary]
-
end
-
end
-
-
1
def add_encoding_to_body
-
if has_content_transfer_encoding?
-
@body.encoding = content_transfer_encoding
-
end
-
end
-
-
1
def identify_and_set_transfer_encoding
-
if body && body.multipart?
-
self.content_transfer_encoding = @transport_encoding
-
else
-
self.content_transfer_encoding = body.negotiate_best_encoding(@transport_encoding, allowed_encodings).to_s
-
end
-
end
-
-
1
def add_required_fields
-
add_required_message_fields
-
add_multipart_mixed_header if body.multipart?
-
add_content_type unless has_content_type?
-
add_charset if text? && !has_charset?
-
add_content_transfer_encoding unless has_content_transfer_encoding?
-
end
-
-
1
def add_required_message_fields
-
add_date unless has_date?
-
add_mime_version unless has_mime_version?
-
add_message_id unless has_message_id?
-
end
-
-
1
def add_multipart_alternate_header
-
header['content-type'] = ContentTypeField.with_boundary('multipart/alternative').value
-
header['content_type'].parameters[:charset] = @charset
-
body.boundary = boundary
-
end
-
-
1
def add_boundary
-
unless body.boundary && boundary
-
header['content-type'] = 'multipart/mixed' unless header['content-type']
-
header['content-type'].parameters[:boundary] = ContentTypeField.generate_boundary
-
header['content_type'].parameters[:charset] = @charset
-
body.boundary = boundary
-
end
-
end
-
-
1
def add_multipart_mixed_header
-
unless header['content-type']
-
header['content-type'] = ContentTypeField.with_boundary('multipart/mixed').value
-
header['content_type'].parameters[:charset] = @charset
-
body.boundary = boundary
-
end
-
end
-
-
1
def init_with_hash(hash)
-
passed_in_options = IndifferentHash.new(hash)
-
self.raw_source = ''
-
-
@header = Mail::Header.new
-
@body = Mail::Body.new
-
@body_raw = nil
-
-
# We need to store the body until last, as we need all headers added first
-
body_content = nil
-
-
passed_in_options.each_pair do |k,v|
-
k = underscoreize(k).to_sym if k.class == String
-
if k == :headers
-
self.headers(v)
-
elsif k == :body
-
body_content = v
-
else
-
self[k] = v
-
end
-
end
-
-
if body_content
-
self.body = body_content
-
if has_content_transfer_encoding?
-
body.encoding = content_transfer_encoding
-
end
-
end
-
end
-
-
1
def init_with_string(string)
-
self.raw_source = string
-
set_envelope_header
-
parse_message
-
@separate_parts = multipart?
-
end
-
-
# Returns the filename of the attachment (if it exists) or returns nil
-
1
def find_attachment
-
content_type_name = header[:content_type].filename rescue nil
-
content_disp_name = header[:content_disposition].filename rescue nil
-
content_loc_name = header[:content_location].location rescue nil
-
case
-
when content_disposition && content_disp_name
-
filename = content_disp_name
-
when content_type && content_type_name
-
filename = content_type_name
-
when content_location && content_loc_name
-
filename = content_loc_name
-
else
-
filename = nil
-
end
-
filename = Mail::Encodings.decode_encode(filename, :decode) if filename rescue filename
-
filename
-
end
-
-
1
def do_delivery
-
begin
-
if perform_deliveries
-
delivery_method.deliver!(self)
-
end
-
rescue => e # Net::SMTP errors or sendmail pipe errors
-
raise e if raise_delivery_errors
-
end
-
end
-
-
1
def decode_body_as_text
-
Encodings.transcode_charset decode_body, charset, 'UTF-8'
-
end
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
1
require 'mail/multibyte/chars'
-
-
1
module Mail #:nodoc:
-
1
module Multibyte
-
# Raised when a problem with the encoding was found.
-
1
class EncodingError < StandardError; end
-
-
1
class << self
-
# The proxy class returned when calling mb_chars. You can use this accessor to configure your own proxy
-
# class so you can support other encodings. See the Mail::Multibyte::Chars implementation for
-
# an example how to do this.
-
#
-
# Example:
-
# Mail::Multibyte.proxy_class = CharsForUTF32
-
1
attr_accessor :proxy_class
-
end
-
-
1
self.proxy_class = Mail::Multibyte::Chars
-
-
1
if RUBY_VERSION >= "1.9"
-
# == Multibyte proxy
-
#
-
# +mb_chars+ is a multibyte safe proxy for string methods.
-
#
-
# In Ruby 1.8 and older it creates and returns an instance of the Mail::Multibyte::Chars class which
-
# encapsulates the original string. A Unicode safe version of all the String methods are defined on this proxy
-
# class. If the proxy class doesn't respond to a certain method, it's forwarded to the encapsuled string.
-
#
-
# name = 'Claus M��ller'
-
# name.reverse # => "rell??M sualC"
-
# name.length # => 13
-
#
-
# name.mb_chars.reverse.to_s # => "rell��M sualC"
-
# name.mb_chars.length # => 12
-
#
-
# In Ruby 1.9 and newer +mb_chars+ returns +self+ because String is (mostly) encoding aware. This means that
-
# it becomes easy to run one version of your code on multiple Ruby versions.
-
#
-
# == Method chaining
-
#
-
# All the methods on the Chars proxy which normally return a string will return a Chars object. This allows
-
# method chaining on the result of any of these methods.
-
#
-
# name.mb_chars.reverse.length # => 12
-
#
-
# == Interoperability and configuration
-
#
-
# The Chars object tries to be as interchangeable with String objects as possible: sorting and comparing between
-
# String and Char work like expected. The bang! methods change the internal string representation in the Chars
-
# object. Interoperability problems can be resolved easily with a +to_s+ call.
-
#
-
# For more information about the methods defined on the Chars proxy see Mail::Multibyte::Chars. For
-
# information about how to change the default Multibyte behaviour see Mail::Multibyte.
-
1
def self.mb_chars(str)
-
if proxy_class.consumes?(str)
-
proxy_class.new(str)
-
else
-
str
-
end
-
end
-
else
-
def self.mb_chars(str)
-
if proxy_class.wants?(str)
-
proxy_class.new(str)
-
else
-
str
-
end
-
end
-
end
-
-
# Regular expressions that describe valid byte sequences for a character
-
1
VALID_CHARACTER = {
-
# Borrowed from the Kconv library by Shinji KONO - (also as seen on the W3C site)
-
'UTF-8' => /\A(?:
-
[\x00-\x7f] |
-
[\xc2-\xdf] [\x80-\xbf] |
-
\xe0 [\xa0-\xbf] [\x80-\xbf] |
-
[\xe1-\xef] [\x80-\xbf] [\x80-\xbf] |
-
\xf0 [\x90-\xbf] [\x80-\xbf] [\x80-\xbf] |
-
[\xf1-\xf3] [\x80-\xbf] [\x80-\xbf] [\x80-\xbf] |
-
\xf4 [\x80-\x8f] [\x80-\xbf] [\x80-\xbf])\z /xn,
-
# Quick check for valid Shift-JIS characters, disregards the odd-even pairing
-
'Shift_JIS' => /\A(?:
-
[\x00-\x7e\xa1-\xdf] |
-
[\x81-\x9f\xe0-\xef] [\x40-\x7e\x80-\x9e\x9f-\xfc])\z /xn
-
}
-
end
-
end
-
-
1
require 'mail/multibyte/utils'
-
# encoding: utf-8
-
# frozen_string_literal: true
-
1
require 'mail/multibyte/unicode'
-
-
1
module Mail #:nodoc:
-
1
module Multibyte #:nodoc:
-
# Chars enables you to work transparently with UTF-8 encoding in the Ruby String class without having extensive
-
# knowledge about the encoding. A Chars object accepts a string upon initialization and proxies String methods in an
-
# encoding safe manner. All the normal String methods are also implemented on the proxy.
-
#
-
# String methods are proxied through the Chars object, and can be accessed through the +mb_chars+ method. Methods
-
# which would normally return a String object now return a Chars object so methods can be chained.
-
#
-
# "The Perfect String ".mb_chars.downcase.strip.normalize # => "the perfect string"
-
#
-
# Chars objects are perfectly interchangeable with String objects as long as no explicit class checks are made.
-
# If certain methods do explicitly check the class, call +to_s+ before you pass chars objects to them.
-
#
-
# bad.explicit_checking_method "T".mb_chars.downcase.to_s
-
#
-
# The default Chars implementation assumes that the encoding of the string is UTF-8, if you want to handle different
-
# encodings you can write your own multibyte string handler and configure it through
-
# Mail::Multibyte.proxy_class.
-
#
-
# class CharsForUTF32
-
# def size
-
# @wrapped_string.size / 4
-
# end
-
#
-
# def self.accepts?(string)
-
# string.length % 4 == 0
-
# end
-
# end
-
#
-
# Mail::Multibyte.proxy_class = CharsForUTF32
-
1
class Chars
-
1
attr_reader :wrapped_string
-
1
alias to_s wrapped_string
-
1
alias to_str wrapped_string
-
-
1
if RUBY_VERSION >= "1.9"
-
# Creates a new Chars instance by wrapping _string_.
-
1
def initialize(string)
-
@wrapped_string = string.dup
-
@wrapped_string.force_encoding(Encoding::UTF_8) unless @wrapped_string.frozen?
-
end
-
else
-
def initialize(string) #:nodoc:
-
@wrapped_string = string
-
end
-
end
-
-
# Forward all undefined methods to the wrapped string.
-
1
def method_missing(method, *args, &block)
-
if method.to_s =~ /!$/
-
@wrapped_string.__send__(method, *args, &block)
-
self
-
else
-
result = @wrapped_string.__send__(method, *args, &block)
-
result.kind_of?(String) ? chars(result) : result
-
end
-
end
-
-
# Returns +true+ if _obj_ responds to the given method. Private methods are included in the search
-
# only if the optional second parameter evaluates to +true+.
-
1
def respond_to?(method, include_private=false)
-
super || @wrapped_string.respond_to?(method, include_private) || false
-
end
-
-
# Enable more predictable duck-typing on String-like classes. See Object#acts_like?.
-
1
def acts_like_string?
-
true
-
end
-
-
# Returns +true+ when the proxy class can handle the string. Returns +false+ otherwise.
-
1
def self.consumes?(string)
-
# Unpack is a little bit faster than regular expressions.
-
string.unpack('U*')
-
true
-
rescue ArgumentError
-
false
-
end
-
-
1
include Comparable
-
-
# Returns -1, 0, or 1, depending on whether the Chars object is to be sorted before,
-
# equal or after the object on the right side of the operation. It accepts any object
-
# that implements +to_s+:
-
#
-
# '��'.mb_chars <=> '��'.mb_chars # => -1
-
#
-
# See <tt>String#<=></tt> for more details.
-
1
def <=>(other)
-
@wrapped_string <=> other.to_s
-
end
-
-
1
if RUBY_VERSION < "1.9"
-
# Returns +true+ if the Chars class can and should act as a proxy for the string _string_. Returns
-
# +false+ otherwise.
-
def self.wants?(string)
-
$KCODE == 'UTF8' && consumes?(string)
-
end
-
-
# Returns a new Chars object containing the _other_ object concatenated to the string.
-
#
-
# Example:
-
# (Mail::Multibyte.mb_chars('Caf��') + ' p��rifer��l').to_s # => "Caf�� p��rifer��l"
-
def +(other)
-
chars(@wrapped_string + other)
-
end
-
-
# Like <tt>String#=~</tt> only it returns the character offset (in codepoints) instead of the byte offset.
-
#
-
# Example:
-
# Mail::Multibyte.mb_chars('Caf�� p��rifer��l') =~ /��/ # => 12
-
def =~(other)
-
translate_offset(@wrapped_string =~ other)
-
end
-
-
# Inserts the passed string at specified codepoint offsets.
-
#
-
# Example:
-
# Mail::Multibyte.mb_chars('Caf��').insert(4, ' p��rifer��l').to_s # => "Caf�� p��rifer��l"
-
def insert(offset, fragment)
-
unpacked = Unicode.u_unpack(@wrapped_string)
-
unless offset > unpacked.length
-
@wrapped_string.replace(
-
Unicode.u_unpack(@wrapped_string).insert(offset, *Unicode.u_unpack(fragment)).pack('U*')
-
)
-
else
-
raise IndexError, "index #{offset} out of string"
-
end
-
self
-
end
-
-
# Returns +true+ if contained string contains _other_. Returns +false+ otherwise.
-
#
-
# Example:
-
# Mail::Multibyte.mb_chars('Caf��').include?('��') # => true
-
def include?(other)
-
# We have to redefine this method because Enumerable defines it.
-
@wrapped_string.include?(other)
-
end
-
-
# Returns the position _needle_ in the string, counting in codepoints. Returns +nil+ if _needle_ isn't found.
-
#
-
# Example:
-
# Mail::Multibyte.mb_chars('Caf�� p��rifer��l').index('��') # => 12
-
# Mail::Multibyte.mb_chars('Caf�� p��rifer��l').index(/\w/u) # => 0
-
def index(needle, offset=0)
-
wrapped_offset = first(offset).wrapped_string.length
-
index = @wrapped_string.index(needle, wrapped_offset)
-
index ? (Unicode.u_unpack(@wrapped_string.slice(0...index)).size) : nil
-
end
-
-
# Returns the position _needle_ in the string, counting in
-
# codepoints, searching backward from _offset_ or the end of the
-
# string. Returns +nil+ if _needle_ isn't found.
-
#
-
# Example:
-
# Mail::Multibyte.mb_chars('Caf�� p��rifer��l').rindex('��') # => 6
-
# Mail::Multibyte.mb_chars('Caf�� p��rifer��l').rindex(/\w/u) # => 13
-
def rindex(needle, offset=nil)
-
offset ||= length
-
wrapped_offset = first(offset).wrapped_string.length
-
index = @wrapped_string.rindex(needle, wrapped_offset)
-
index ? (Unicode.u_unpack(@wrapped_string.slice(0...index)).size) : nil
-
end
-
-
# Returns the number of codepoints in the string
-
def size
-
Unicode.u_unpack(@wrapped_string).size
-
end
-
alias_method :length, :size
-
-
# Strips entire range of Unicode whitespace from the right of the string.
-
def rstrip
-
chars(@wrapped_string.gsub(Unicode::TRAILERS_PAT, ''))
-
end
-
-
# Strips entire range of Unicode whitespace from the left of the string.
-
def lstrip
-
chars(@wrapped_string.gsub(Unicode::LEADERS_PAT, ''))
-
end
-
-
# Strips entire range of Unicode whitespace from the right and left of the string.
-
def strip
-
rstrip.lstrip
-
end
-
-
# Returns the codepoint of the first character in the string.
-
#
-
# Example:
-
# Mail::Multibyte.mb_chars('���������������').ord # => 12371
-
def ord
-
Unicode.u_unpack(@wrapped_string)[0]
-
end
-
-
# Works just like <tt>String#rjust</tt>, only integer specifies characters instead of bytes.
-
#
-
# Example:
-
#
-
# Mail::Multibyte.mb_chars("�� cup").rjust(8).to_s
-
# # => " �� cup"
-
#
-
# Mail::Multibyte.mb_chars("�� cup").rjust(8, "��").to_s # Use non-breaking whitespace
-
# # => "�������� cup"
-
def rjust(integer, padstr=' ')
-
justify(integer, :right, padstr)
-
end
-
-
# Works just like <tt>String#ljust</tt>, only integer specifies characters instead of bytes.
-
#
-
# Example:
-
#
-
# Mail::Multibyte.mb_chars("�� cup").rjust(8).to_s
-
# # => "�� cup "
-
#
-
# Mail::Multibyte.mb_chars("�� cup").rjust(8, "��").to_s # Use non-breaking whitespace
-
# # => "�� cup������"
-
def ljust(integer, padstr=' ')
-
justify(integer, :left, padstr)
-
end
-
-
# Works just like <tt>String#center</tt>, only integer specifies characters instead of bytes.
-
#
-
# Example:
-
#
-
# Mail::Multibyte.mb_chars("�� cup").center(8).to_s
-
# # => " �� cup "
-
#
-
# Mail::Multibyte.mb_chars("�� cup").center(8, "��").to_s # Use non-breaking whitespace
-
# # => "���� cup����"
-
def center(integer, padstr=' ')
-
justify(integer, :center, padstr)
-
end
-
-
else
-
1
def =~(other)
-
@wrapped_string =~ other
-
end
-
end
-
-
# Works just like <tt>String#split</tt>, with the exception that the items in the resulting list are Chars
-
# instances instead of String. This makes chaining methods easier.
-
#
-
# Example:
-
# Mail::Multibyte.mb_chars('Caf�� p��rifer��l').split(/��/).map { |part| part.upcase.to_s } # => ["CAF", " P", "RIFER��L"]
-
1
def split(*args)
-
@wrapped_string.split(*args).map { |i| i.mb_chars }
-
end
-
-
# Like <tt>String#[]=</tt>, except instead of byte offsets you specify character offsets.
-
#
-
# Example:
-
#
-
# s = "M��ller"
-
# s.mb_chars[2] = "e" # Replace character with offset 2
-
# s
-
# # => "M��eler"
-
#
-
# s = "M��ller"
-
# s.mb_chars[1, 2] = "��" # Replace 2 characters at character offset 1
-
# s
-
# # => "M��ler"
-
1
def []=(*args)
-
replace_by = args.pop
-
# Indexed replace with regular expressions already works
-
if args.first.is_a?(Regexp)
-
@wrapped_string[*args] = replace_by
-
else
-
result = Unicode.u_unpack(@wrapped_string)
-
if args[0].is_a?(Integer)
-
raise IndexError, "index #{args[0]} out of string" if args[0] >= result.length
-
min = args[0]
-
max = args[1].nil? ? min : (min + args[1] - 1)
-
range = Range.new(min, max)
-
replace_by = [replace_by].pack('U') if replace_by.is_a?(Integer)
-
elsif args.first.is_a?(Range)
-
raise RangeError, "#{args[0]} out of range" if args[0].min >= result.length
-
range = args[0]
-
else
-
needle = args[0].to_s
-
min = index(needle)
-
max = min + Unicode.u_unpack(needle).length - 1
-
range = Range.new(min, max)
-
end
-
result[range] = Unicode.u_unpack(replace_by)
-
@wrapped_string.replace(result.pack('U*'))
-
end
-
end
-
-
# Reverses all characters in the string.
-
#
-
# Example:
-
# Mail::Multibyte.mb_chars('Caf��').reverse.to_s # => '��faC'
-
1
def reverse
-
chars(Unicode.g_unpack(@wrapped_string).reverse.flatten.pack('U*'))
-
end
-
-
# Implements Unicode-aware slice with codepoints. Slicing on one point returns the codepoints for that
-
# character.
-
#
-
# Example:
-
# Mail::Multibyte.mb_chars('���������������').slice(2..3).to_s # => "������"
-
1
def slice(*args)
-
if args.size > 2
-
raise ArgumentError, "wrong number of arguments (#{args.size} for 1)" # Do as if we were native
-
elsif (args.size == 2 && !(args.first.is_a?(Numeric) || args.first.is_a?(Regexp)))
-
raise TypeError, "cannot convert #{args.first.class} into Integer" # Do as if we were native
-
elsif (args.size == 2 && !args[1].is_a?(Numeric))
-
raise TypeError, "cannot convert #{args[1].class} into Integer" # Do as if we were native
-
elsif args[0].kind_of? Range
-
cps = Unicode.u_unpack(@wrapped_string).slice(*args)
-
result = cps.nil? ? nil : cps.pack('U*')
-
elsif args[0].kind_of? Regexp
-
result = @wrapped_string.slice(*args)
-
elsif args.size == 1 && args[0].kind_of?(Numeric)
-
character = Unicode.u_unpack(@wrapped_string)[args[0]]
-
result = character && [character].pack('U')
-
else
-
cps = Unicode.u_unpack(@wrapped_string).slice(*args)
-
result = cps && cps.pack('U*')
-
end
-
result && chars(result)
-
end
-
1
alias_method :[], :slice
-
-
# Limit the byte size of the string to a number of bytes without breaking characters. Usable
-
# when the storage for a string is limited for some reason.
-
#
-
# Example:
-
# s = '���������������'
-
# s.mb_chars.limit(7) # => "������"
-
1
def limit(limit)
-
slice(0...translate_offset(limit))
-
end
-
-
# Convert characters in the string to uppercase.
-
#
-
# Example:
-
# Mail::Multibyte.mb_chars('Laurent, o�� sont les tests ?').upcase.to_s # => "LAURENT, O�� SONT LES TESTS ?"
-
1
def upcase
-
chars(Unicode.apply_mapping(@wrapped_string, :uppercase_mapping))
-
end
-
-
# Convert characters in the string to lowercase.
-
#
-
# Example:
-
# Mail::Multibyte.mb_chars('V��DA A V��ZKUM').downcase.to_s # => "v��da a v��zkum"
-
1
def downcase
-
chars(Unicode.apply_mapping(@wrapped_string, :lowercase_mapping))
-
end
-
-
# Converts the first character to uppercase and the remainder to lowercase.
-
#
-
# Example:
-
# Mail::Multibyte.mb_chars('��ber').capitalize.to_s # => "��ber"
-
1
def capitalize
-
(slice(0) || chars('')).upcase + (slice(1..-1) || chars('')).downcase
-
end
-
-
# Capitalizes the first letter of every word, when possible.
-
#
-
# Example:
-
# Mail::Multibyte.mb_chars("��L QUE SE ENTER��").titleize # => "��l Que Se Enter��"
-
# Mail::Multibyte.mb_chars("���������").titleize # => "���������"
-
1
def titleize
-
chars(downcase.to_s.gsub(/\b('?\S)/u) { Unicode.apply_mapping $1, :uppercase_mapping })
-
end
-
1
alias_method :titlecase, :titleize
-
-
# Returns the KC normalization of the string by default. NFKC is considered the best normalization form for
-
# passing strings to databases and validations.
-
#
-
# * <tt>form</tt> - The form you want to normalize in. Should be one of the following:
-
# <tt>:c</tt>, <tt>:kc</tt>, <tt>:d</tt>, or <tt>:kd</tt>. Default is
-
# Mail::Multibyte::Unicode.default_normalization_form
-
1
def normalize(form = nil)
-
chars(Unicode.normalize(@wrapped_string, form))
-
end
-
-
# Performs canonical decomposition on all the characters.
-
#
-
# Example:
-
# '��'.length # => 2
-
# Mail::Multibyte.mb_chars('��').decompose.to_s.length # => 3
-
1
def decompose
-
chars(Unicode.decompose_codepoints(:canonical, Unicode.u_unpack(@wrapped_string)).pack('U*'))
-
end
-
-
# Performs composition on all the characters.
-
#
-
# Example:
-
# '��'.length # => 3
-
# Mail::Multibyte.mb_chars('��').compose.to_s.length # => 2
-
1
def compose
-
chars(Unicode.compose_codepoints(Unicode.u_unpack(@wrapped_string)).pack('U*'))
-
end
-
-
# Returns the number of grapheme clusters in the string.
-
#
-
# Example:
-
# Mail::Multibyte.mb_chars('������������').length # => 4
-
# Mail::Multibyte.mb_chars('������������').g_length # => 3
-
1
def g_length
-
Unicode.g_unpack(@wrapped_string).length
-
end
-
-
# Replaces all ISO-8859-1 or CP1252 characters by their UTF-8 equivalent resulting in a valid UTF-8 string.
-
#
-
# Passing +true+ will forcibly tidy all bytes, assuming that the string's encoding is entirely CP1252 or ISO-8859-1.
-
1
def tidy_bytes(force = false)
-
chars(Unicode.tidy_bytes(@wrapped_string, force))
-
end
-
-
1
%w(capitalize downcase lstrip reverse rstrip slice strip tidy_bytes upcase).each do |method|
-
# Only define a corresponding bang method for methods defined in the proxy; On 1.9 the proxy will
-
# exclude lstrip!, rstrip! and strip! because they are already work as expected on multibyte strings.
-
9
if public_method_defined?(method)
-
6
define_method("#{method}!") do |*args|
-
@wrapped_string = send(args.nil? ? method : method, *args).to_s
-
self
-
end
-
end
-
end
-
-
1
protected
-
-
1
def translate_offset(byte_offset) #:nodoc:
-
return nil if byte_offset.nil?
-
return 0 if @wrapped_string == ''
-
-
if @wrapped_string.respond_to?(:force_encoding)
-
@wrapped_string = @wrapped_string.dup.force_encoding(Encoding::ASCII_8BIT)
-
end
-
-
begin
-
@wrapped_string[0...byte_offset].unpack('U*').length
-
rescue ArgumentError
-
byte_offset -= 1
-
retry
-
end
-
end
-
-
1
def justify(integer, way, padstr=' ') #:nodoc:
-
raise ArgumentError, "zero width padding" if padstr.length == 0
-
padsize = integer - size
-
padsize = padsize > 0 ? padsize : 0
-
case way
-
when :right
-
result = @wrapped_string.dup.insert(0, padding(padsize, padstr))
-
when :left
-
result = @wrapped_string.dup.insert(-1, padding(padsize, padstr))
-
when :center
-
lpad = padding((padsize / 2.0).floor, padstr)
-
rpad = padding((padsize / 2.0).ceil, padstr)
-
result = @wrapped_string.dup.insert(0, lpad).insert(-1, rpad)
-
end
-
chars(result)
-
end
-
-
1
def padding(padsize, padstr=' ') #:nodoc:
-
if padsize != 0
-
chars(padstr * ((padsize / Unicode.u_unpack(padstr).size) + 1)).slice(0, padsize)
-
else
-
''
-
end
-
end
-
-
1
def chars(string) #:nodoc:
-
self.class.new(string)
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
module Mail
-
1
module Multibyte
-
1
module Unicode
-
# Adapted from https://github.com/rails/rails/blob/master/activesupport/lib/active_support/multibyte/unicode.rb
-
# under the MIT license
-
# The Unicode version that is supported by the implementation
-
1
UNICODE_VERSION = '7.0.0'
-
-
# Holds data about a codepoint in the Unicode database.
-
1
class Codepoint
-
1
attr_accessor :code, :combining_class, :decomp_type, :decomp_mapping, :uppercase_mapping, :lowercase_mapping
-
-
# Initializing Codepoint object with default values
-
1
def initialize
-
@combining_class = 0
-
@uppercase_mapping = 0
-
@lowercase_mapping = 0
-
end
-
-
1
def swapcase_mapping
-
uppercase_mapping > 0 ? uppercase_mapping : lowercase_mapping
-
end
-
end
-
-
1
extend self
-
-
# A list of all available normalization forms. See http://www.unicode.org/reports/tr15/tr15-29.html for more
-
# information about normalization.
-
1
NORMALIZATION_FORMS = [:c, :kc, :d, :kd]
-
-
# The default normalization used for operations that require normalization. It can be set to any of the
-
# normalizations in NORMALIZATION_FORMS.
-
#
-
# Example:
-
# Mail::Multibyte::Unicode.default_normalization_form = :c
-
1
attr_accessor :default_normalization_form
-
1
@default_normalization_form = :kc
-
-
# Hangul character boundaries and properties
-
1
HANGUL_SBASE = 0xAC00
-
1
HANGUL_LBASE = 0x1100
-
1
HANGUL_VBASE = 0x1161
-
1
HANGUL_TBASE = 0x11A7
-
1
HANGUL_LCOUNT = 19
-
1
HANGUL_VCOUNT = 21
-
1
HANGUL_TCOUNT = 28
-
1
HANGUL_NCOUNT = HANGUL_VCOUNT * HANGUL_TCOUNT
-
1
HANGUL_SCOUNT = 11172
-
1
HANGUL_SLAST = HANGUL_SBASE + HANGUL_SCOUNT
-
1
HANGUL_JAMO_FIRST = 0x1100
-
1
HANGUL_JAMO_LAST = 0x11FF
-
-
# All the unicode whitespace
-
WHITESPACE = [
-
2
(0x0009..0x000D).to_a, # White_Space # Cc [5] <control-0009>..<control-000D>
-
0x0020, # White_Space # Zs SPACE
-
0x0085, # White_Space # Cc <control-0085>
-
0x00A0, # White_Space # Zs NO-BREAK SPACE
-
0x1680, # White_Space # Zs OGHAM SPACE MARK
-
0x180E, # White_Space # Zs MONGOLIAN VOWEL SEPARATOR
-
1
(0x2000..0x200A).to_a, # White_Space # Zs [11] EN QUAD..HAIR SPACE
-
0x2028, # White_Space # Zl LINE SEPARATOR
-
0x2029, # White_Space # Zp PARAGRAPH SEPARATOR
-
0x202F, # White_Space # Zs NARROW NO-BREAK SPACE
-
0x205F, # White_Space # Zs MEDIUM MATHEMATICAL SPACE
-
0x3000, # White_Space # Zs IDEOGRAPHIC SPACE
-
].flatten.freeze
-
-
# BOM (byte order mark) can also be seen as whitespace, it's a non-rendering character used to distinguish
-
# between little and big endian. This is not an issue in utf-8, so it must be ignored.
-
1
LEADERS_AND_TRAILERS = WHITESPACE + [65279] # ZERO-WIDTH NO-BREAK SPACE aka BOM
-
-
# Returns a regular expression pattern that matches the passed Unicode codepoints
-
1
def self.codepoints_to_pattern(array_of_codepoints) #:nodoc:
-
56
array_of_codepoints.collect{ |e| [e].pack 'U*' }.join('|')
-
end
-
1
TRAILERS_PAT = /(#{codepoints_to_pattern(LEADERS_AND_TRAILERS)})+\Z/u
-
1
LEADERS_PAT = /\A(#{codepoints_to_pattern(LEADERS_AND_TRAILERS)})+/u
-
-
# Unpack the string at codepoints boundaries. Raises an EncodingError when the encoding of the string isn't
-
# valid UTF-8.
-
#
-
# Example:
-
# Unicode.u_unpack('Caf��') # => [67, 97, 102, 233]
-
1
def u_unpack(string)
-
begin
-
string.unpack 'U*'
-
rescue ArgumentError
-
raise EncodingError, 'malformed UTF-8 character'
-
end
-
end
-
-
# Detect whether the codepoint is in a certain character class. Returns +true+ when it's in the specified
-
# character class and +false+ otherwise. Valid character classes are: <tt>:cr</tt>, <tt>:lf</tt>, <tt>:l</tt>,
-
# <tt>:v</tt>, <tt>:lv</tt>, <tt>:lvt</tt> and <tt>:t</tt>.
-
#
-
# Primarily used by the grapheme cluster support.
-
1
def in_char_class?(codepoint, classes)
-
classes.detect { |c| database.boundary[c] === codepoint } ? true : false
-
end
-
-
# Unpack the string at grapheme boundaries. Returns a list of character lists.
-
#
-
# Example:
-
# Unicode.g_unpack('������������') # => [[2325, 2381], [2359], [2367]]
-
# Unicode.g_unpack('Caf��') # => [[67], [97], [102], [233]]
-
1
def g_unpack(string)
-
codepoints = u_unpack(string)
-
unpacked = []
-
pos = 0
-
marker = 0
-
eoc = codepoints.length
-
while(pos < eoc)
-
pos += 1
-
previous = codepoints[pos-1]
-
current = codepoints[pos]
-
if (
-
# CR X LF
-
( previous == database.boundary[:cr] and current == database.boundary[:lf] ) or
-
# L X (L|V|LV|LVT)
-
( database.boundary[:l] === previous and in_char_class?(current, [:l,:v,:lv,:lvt]) ) or
-
# (LV|V) X (V|T)
-
( in_char_class?(previous, [:lv,:v]) and in_char_class?(current, [:v,:t]) ) or
-
# (LVT|T) X (T)
-
( in_char_class?(previous, [:lvt,:t]) and database.boundary[:t] === current ) or
-
# X Extend
-
(database.boundary[:extend] === current)
-
)
-
else
-
unpacked << codepoints[marker..pos-1]
-
marker = pos
-
end
-
end
-
unpacked
-
end
-
-
# Reverse operation of g_unpack.
-
#
-
# Example:
-
# Unicode.g_pack(Unicode.g_unpack('������������')) # => '������������'
-
1
def g_pack(unpacked)
-
(unpacked.flatten).pack('U*')
-
end
-
-
# Re-order codepoints so the string becomes canonical.
-
1
def reorder_characters(codepoints)
-
length = codepoints.length- 1
-
pos = 0
-
while pos < length do
-
cp1, cp2 = database.codepoints[codepoints[pos]], database.codepoints[codepoints[pos+1]]
-
if (cp1.combining_class > cp2.combining_class) && (cp2.combining_class > 0)
-
codepoints[pos..pos+1] = cp2.code, cp1.code
-
pos += (pos > 0 ? -1 : 1)
-
else
-
pos += 1
-
end
-
end
-
codepoints
-
end
-
-
# Decompose composed characters to the decomposed form.
-
1
def decompose_codepoints(type, codepoints)
-
codepoints.inject([]) do |decomposed, cp|
-
# if it's a hangul syllable starter character
-
if HANGUL_SBASE <= cp and cp < HANGUL_SLAST
-
sindex = cp - HANGUL_SBASE
-
ncp = [] # new codepoints
-
ncp << HANGUL_LBASE + sindex / HANGUL_NCOUNT
-
ncp << HANGUL_VBASE + (sindex % HANGUL_NCOUNT) / HANGUL_TCOUNT
-
tindex = sindex % HANGUL_TCOUNT
-
ncp << (HANGUL_TBASE + tindex) unless tindex == 0
-
decomposed.concat ncp
-
# if the codepoint is decomposable in with the current decomposition type
-
elsif (ncp = database.codepoints[cp].decomp_mapping) and (!database.codepoints[cp].decomp_type || type == :compatability)
-
decomposed.concat decompose_codepoints(type, ncp.dup)
-
else
-
decomposed << cp
-
end
-
end
-
end
-
-
# Compose decomposed characters to the composed form.
-
1
def compose_codepoints(codepoints)
-
pos = 0
-
eoa = codepoints.length - 1
-
starter_pos = 0
-
starter_char = codepoints[0]
-
previous_combining_class = -1
-
while pos < eoa
-
pos += 1
-
lindex = starter_char - HANGUL_LBASE
-
# -- Hangul
-
if 0 <= lindex and lindex < HANGUL_LCOUNT
-
vindex = codepoints[starter_pos+1] - HANGUL_VBASE rescue vindex = -1
-
if 0 <= vindex and vindex < HANGUL_VCOUNT
-
tindex = codepoints[starter_pos+2] - HANGUL_TBASE rescue tindex = -1
-
if 0 <= tindex and tindex < HANGUL_TCOUNT
-
j = starter_pos + 2
-
eoa -= 2
-
else
-
tindex = 0
-
j = starter_pos + 1
-
eoa -= 1
-
end
-
codepoints[starter_pos..j] = (lindex * HANGUL_VCOUNT + vindex) * HANGUL_TCOUNT + tindex + HANGUL_SBASE
-
end
-
starter_pos += 1
-
starter_char = codepoints[starter_pos]
-
# -- Other characters
-
else
-
current_char = codepoints[pos]
-
current = database.codepoints[current_char]
-
if current.combining_class > previous_combining_class
-
if ref = database.composition_map[starter_char]
-
composition = ref[current_char]
-
else
-
composition = nil
-
end
-
unless composition.nil?
-
codepoints[starter_pos] = composition
-
starter_char = composition
-
codepoints.delete_at pos
-
eoa -= 1
-
pos -= 1
-
previous_combining_class = -1
-
else
-
previous_combining_class = current.combining_class
-
end
-
else
-
previous_combining_class = current.combining_class
-
end
-
if current.combining_class == 0
-
starter_pos = pos
-
starter_char = codepoints[pos]
-
end
-
end
-
end
-
codepoints
-
end
-
-
# Replaces all ISO-8859-1 or CP1252 characters by their UTF-8 equivalent resulting in a valid UTF-8 string.
-
#
-
# Passing +true+ will forcibly tidy all bytes, assuming that the string's encoding is entirely CP1252 or ISO-8859-1.
-
1
def tidy_bytes(string, force = false)
-
if force
-
return string.unpack("C*").map do |b|
-
tidy_byte(b)
-
end.flatten.compact.pack("C*").unpack("U*").pack("U*")
-
end
-
-
bytes = string.unpack("C*")
-
conts_expected = 0
-
last_lead = 0
-
-
bytes.each_index do |i|
-
-
byte = bytes[i]
-
is_cont = byte > 127 && byte < 192
-
is_lead = byte > 191 && byte < 245
-
is_unused = byte > 240
-
is_restricted = byte > 244
-
-
# Impossible or highly unlikely byte? Clean it.
-
if is_unused || is_restricted
-
bytes[i] = tidy_byte(byte)
-
elsif is_cont
-
# Not expecting contination byte? Clean up. Otherwise, now expect one less.
-
conts_expected == 0 ? bytes[i] = tidy_byte(byte) : conts_expected -= 1
-
else
-
if conts_expected > 0
-
# Expected continuation, but got ASCII or leading? Clean backwards up to
-
# the leading byte.
-
(1..(i - last_lead)).each {|j| bytes[i - j] = tidy_byte(bytes[i - j])}
-
conts_expected = 0
-
end
-
if is_lead
-
# Final byte is leading? Clean it.
-
if i == bytes.length - 1
-
bytes[i] = tidy_byte(bytes.last)
-
else
-
# Valid leading byte? Expect continuations determined by position of
-
# first zero bit, with max of 3.
-
conts_expected = byte < 224 ? 1 : byte < 240 ? 2 : 3
-
last_lead = i
-
end
-
end
-
end
-
end
-
bytes.empty? ? "" : bytes.flatten.compact.pack("C*").unpack("U*").pack("U*")
-
end
-
-
# Returns the KC normalization of the string by default. NFKC is considered the best normalization form for
-
# passing strings to databases and validations.
-
#
-
# * <tt>string</tt> - The string to perform normalization on.
-
# * <tt>form</tt> - The form you want to normalize in. Should be one of the following:
-
# <tt>:c</tt>, <tt>:kc</tt>, <tt>:d</tt>, or <tt>:kd</tt>. Default is
-
# Mail::Multibyte.default_normalization_form
-
1
def normalize(string, form=nil)
-
form ||= @default_normalization_form
-
# See http://www.unicode.org/reports/tr15, Table 1
-
codepoints = u_unpack(string)
-
case form
-
when :d
-
reorder_characters(decompose_codepoints(:canonical, codepoints))
-
when :c
-
compose_codepoints(reorder_characters(decompose_codepoints(:canonical, codepoints)))
-
when :kd
-
reorder_characters(decompose_codepoints(:compatability, codepoints))
-
when :kc
-
compose_codepoints(reorder_characters(decompose_codepoints(:compatability, codepoints)))
-
else
-
raise ArgumentError, "#{form} is not a valid normalization variant", caller
-
end.pack('U*')
-
end
-
-
1
def apply_mapping(string, mapping) #:nodoc:
-
u_unpack(string).map do |codepoint|
-
cp = database.codepoints[codepoint]
-
if cp and (ncp = cp.send(mapping)) and ncp > 0
-
ncp
-
else
-
codepoint
-
end
-
end.pack('U*')
-
end
-
-
# Holds static data from the Unicode database
-
1
class UnicodeDatabase
-
1
ATTRIBUTES = :codepoints, :composition_exclusion, :composition_map, :boundary, :cp1252
-
-
1
attr_writer(*ATTRIBUTES)
-
-
1
def initialize
-
@codepoints = Hash.new(Codepoint.new)
-
@composition_exclusion = []
-
@composition_map = {}
-
@boundary = {}
-
@cp1252 = {}
-
end
-
-
# Lazy load the Unicode database so it's only loaded when it's actually used
-
1
ATTRIBUTES.each do |attr_name|
-
5
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
-
5
def #{attr_name} # def codepoints
-
load # load
-
5
@#{attr_name} # @codepoints
-
end # end
-
EOS
-
end
-
-
# Loads the Unicode database and returns all the internal objects of UnicodeDatabase.
-
1
def load
-
begin
-
@codepoints, @composition_exclusion, @composition_map, @boundary, @cp1252 = File.open(self.class.filename, 'rb') { |f| Marshal.load f.read }
-
rescue => e
-
raise IOError.new("Couldn't load the Unicode tables for UTF8Handler (#{e.message}), Mail::Multibyte is unusable")
-
end
-
-
# Redefine the === method so we can write shorter rules for grapheme cluster breaks
-
@boundary.each do |k,_|
-
@boundary[k].instance_eval do
-
def ===(other)
-
detect { |i| i === other } ? true : false
-
end
-
end if @boundary[k].kind_of?(Array)
-
end
-
-
# define attr_reader methods for the instance variables
-
class << self
-
attr_reader(*ATTRIBUTES)
-
end
-
end
-
-
# Returns the directory in which the data files are stored
-
1
def self.dirname
-
File.dirname(__FILE__) + '/../values/'
-
end
-
-
# Returns the filename for the data file for this version
-
1
def self.filename
-
File.expand_path File.join(dirname, "unicode_tables.dat")
-
end
-
end
-
-
1
private
-
-
1
def tidy_byte(byte)
-
if byte < 160
-
[database.cp1252[byte] || byte].pack("U").unpack("C*")
-
elsif byte < 192
-
[194, byte]
-
else
-
[195, byte - 64]
-
end
-
end
-
-
1
def database
-
@database ||= UnicodeDatabase.new
-
end
-
-
end
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
-
1
module Mail #:nodoc:
-
1
module Multibyte #:nodoc:
-
1
if RUBY_VERSION >= "1.9"
-
# Returns a regular expression that matches valid characters in the current encoding
-
1
def self.valid_character
-
VALID_CHARACTER[Encoding.default_external.to_s]
-
end
-
else
-
def self.valid_character
-
case $KCODE
-
when 'UTF8'
-
VALID_CHARACTER['UTF-8']
-
when 'SJIS'
-
VALID_CHARACTER['Shift_JIS']
-
end
-
end
-
end
-
-
1
if 'string'.respond_to?(:valid_encoding?)
-
# Verifies the encoding of a string
-
1
def self.verify(string)
-
string.valid_encoding?
-
end
-
else
-
def self.verify(string)
-
if expression = valid_character
-
# Splits the string on character boundaries, which are determined based on $KCODE.
-
string.split(//).all? { |c| expression =~ c }
-
else
-
true
-
end
-
end
-
end
-
-
# Verifies the encoding of the string and raises an exception when it's not valid
-
1
def self.verify!(string)
-
raise EncodingError.new("Found characters with invalid encoding") unless verify(string)
-
end
-
-
1
if 'string'.respond_to?(:force_encoding)
-
# Removes all invalid characters from the string.
-
#
-
# Note: this method is a no-op in Ruby 1.9
-
1
def self.clean(string)
-
string
-
end
-
else
-
def self.clean(string)
-
if expression = valid_character
-
# Splits the string on character boundaries, which are determined based on $KCODE.
-
string.split(//).grep(expression).join
-
else
-
string
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'mail/network/retriever_methods/base'
-
-
1
module Mail
-
1
register_autoload :SMTP, 'mail/network/delivery_methods/smtp'
-
1
register_autoload :FileDelivery, 'mail/network/delivery_methods/file_delivery'
-
1
register_autoload :LoggerDelivery, 'mail/network/delivery_methods/logger_delivery'
-
1
register_autoload :Sendmail, 'mail/network/delivery_methods/sendmail'
-
1
register_autoload :Exim, 'mail/network/delivery_methods/exim'
-
1
register_autoload :SMTPConnection, 'mail/network/delivery_methods/smtp_connection'
-
1
register_autoload :TestMailer, 'mail/network/delivery_methods/test_mailer'
-
-
1
register_autoload :POP3, 'mail/network/retriever_methods/pop3'
-
1
register_autoload :IMAP, 'mail/network/retriever_methods/imap'
-
1
register_autoload :TestRetriever, 'mail/network/retriever_methods/test_retriever'
-
end
-
# frozen_string_literal: true
-
1
require 'mail/check_delivery_params'
-
-
1
module Mail
-
# FileDelivery class delivers emails into multiple files based on the destination
-
# address. Each file is appended to if it already exists.
-
#
-
# So if you have an email going to fred@test, bob@test, joe@anothertest, and you
-
# set your location path to /path/to/mails then FileDelivery will create the directory
-
# if it does not exist, and put one copy of the email in three files, called
-
# by their message id
-
#
-
# Make sure the path you specify with :location is writable by the Ruby process
-
# running Mail.
-
1
class FileDelivery
-
1
if RUBY_VERSION >= '1.9.1'
-
1
require 'fileutils'
-
else
-
require 'ftools'
-
end
-
-
1
attr_accessor :settings
-
-
1
def initialize(values)
-
self.settings = { :location => './mails' }.merge!(values)
-
end
-
-
1
def deliver!(mail)
-
Mail::CheckDeliveryParams.check(mail)
-
-
if ::File.respond_to?(:makedirs)
-
::File.makedirs settings[:location]
-
else
-
::FileUtils.mkdir_p settings[:location]
-
end
-
-
mail.destinations.uniq.each do |to|
-
::File.open(::File.join(settings[:location], File.basename(to.to_s)), 'a') { |f| "#{f.write(mail.encoded)}\r\n\r\n" }
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'mail/check_delivery_params'
-
-
1
module Mail
-
# A delivery method implementation which sends via sendmail.
-
#
-
# To use this, first find out where the sendmail binary is on your computer,
-
# if you are on a mac or unix box, it is usually in /usr/sbin/sendmail, this will
-
# be your sendmail location.
-
#
-
# Mail.defaults do
-
# delivery_method :sendmail
-
# end
-
#
-
# Or if your sendmail binary is not at '/usr/sbin/sendmail'
-
#
-
# Mail.defaults do
-
# delivery_method :sendmail, :location => '/absolute/path/to/your/sendmail'
-
# end
-
#
-
# Then just deliver the email as normal:
-
#
-
# Mail.deliver do
-
# to 'mikel@test.lindsaar.net'
-
# from 'ada@test.lindsaar.net'
-
# subject 'testing sendmail'
-
# body 'testing sendmail'
-
# end
-
#
-
# Or by calling deliver on a Mail message
-
#
-
# mail = Mail.new do
-
# to 'mikel@test.lindsaar.net'
-
# from 'ada@test.lindsaar.net'
-
# subject 'testing sendmail'
-
# body 'testing sendmail'
-
# end
-
#
-
# mail.deliver!
-
1
class Sendmail
-
1
DEFAULTS = {
-
:location => '/usr/sbin/sendmail',
-
:arguments => '-i'
-
}
-
-
1
attr_accessor :settings
-
-
1
def initialize(values)
-
self.settings = self.class::DEFAULTS.merge(values)
-
end
-
-
1
def deliver!(mail)
-
smtp_from, smtp_to, message = Mail::CheckDeliveryParams.check(mail)
-
-
from = "-f #{self.class.shellquote(smtp_from)}" if smtp_from
-
to = smtp_to.map { |_to| self.class.shellquote(_to) }.join(' ')
-
-
arguments = "#{settings[:arguments]} #{from} --"
-
self.class.call(settings[:location], arguments, to, message)
-
end
-
-
1
def self.call(path, arguments, destinations, encoded_message)
-
popen "#{path} #{arguments} #{destinations}" do |io|
-
io.puts ::Mail::Utilities.binary_unsafe_to_lf(encoded_message)
-
io.flush
-
end
-
end
-
-
1
if RUBY_VERSION < '1.9.0'
-
def self.popen(command, &block)
-
IO.popen "#{command} 2>&1", 'w+', &block
-
end
-
else
-
1
def self.popen(command, &block)
-
IO.popen command, 'w+', :err => :out, &block
-
end
-
end
-
-
# The following is an adaptation of ruby 1.9.2's shellwords.rb file,
-
# with the following modifications:
-
#
-
# - Wraps in double quotes
-
# - Allows '+' to accept email addresses with them
-
# - Allows '~' as it is not unescaped in double quotes
-
1
def self.shellquote(address)
-
# Process as a single byte sequence because not all shell
-
# implementations are multibyte aware.
-
#
-
# A LF cannot be escaped with a backslash because a backslash + LF
-
# combo is regarded as line continuation and simply ignored. Strip it.
-
escaped = address.gsub(/([^A-Za-z0-9_\s\+\-.,:\/@~])/n, "\\\\\\1").gsub("\n", '')
-
%("#{escaped}")
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'mail/check_delivery_params'
-
-
1
module Mail
-
# == Sending Email with SMTP
-
#
-
# Mail allows you to send emails using SMTP. This is done by wrapping Net::SMTP in
-
# an easy to use manner.
-
#
-
# === Sending via SMTP server on Localhost
-
#
-
# Sending locally (to a postfix or sendmail server running on localhost) requires
-
# no special setup. Just to Mail.deliver &block or message.deliver! and it will
-
# be sent in this method.
-
#
-
# === Sending via MobileMe
-
#
-
# Mail.defaults do
-
# delivery_method :smtp, { :address => "smtp.me.com",
-
# :port => 587,
-
# :domain => 'your.host.name',
-
# :user_name => '<username>',
-
# :password => '<password>',
-
# :authentication => 'plain',
-
# :enable_starttls_auto => true }
-
# end
-
#
-
# === Sending via GMail
-
#
-
# Mail.defaults do
-
# delivery_method :smtp, { :address => "smtp.gmail.com",
-
# :port => 587,
-
# :domain => 'your.host.name',
-
# :user_name => '<username>',
-
# :password => '<password>',
-
# :authentication => 'plain',
-
# :enable_starttls_auto => true }
-
# end
-
#
-
# === Certificate verification
-
#
-
# When using TLS, some mail servers provide certificates that are self-signed
-
# or whose names do not exactly match the hostname given in the address.
-
# OpenSSL will reject these by default. The best remedy is to use the correct
-
# hostname or update the certificate authorities trusted by your ruby. If
-
# that isn't possible, you can control this behavior with
-
# an :openssl_verify_mode setting. Its value may be either an OpenSSL
-
# verify mode constant (OpenSSL::SSL::VERIFY_NONE, OpenSSL::SSL::VERIFY_PEER),
-
# or a string containing the name of an OpenSSL verify mode (none, peer).
-
#
-
# === Others
-
#
-
# Feel free to send me other examples that were tricky
-
#
-
# === Delivering the email
-
#
-
# Once you have the settings right, sending the email is done by:
-
#
-
# Mail.deliver do
-
# to 'mikel@test.lindsaar.net'
-
# from 'ada@test.lindsaar.net'
-
# subject 'testing sendmail'
-
# body 'testing sendmail'
-
# end
-
#
-
# Or by calling deliver on a Mail message
-
#
-
# mail = Mail.new do
-
# to 'mikel@test.lindsaar.net'
-
# from 'ada@test.lindsaar.net'
-
# subject 'testing sendmail'
-
# body 'testing sendmail'
-
# end
-
#
-
# mail.deliver!
-
1
class SMTP
-
1
attr_accessor :settings
-
-
1
DEFAULTS = {
-
:address => 'localhost',
-
:port => 25,
-
:domain => 'localhost.localdomain',
-
:user_name => nil,
-
:password => nil,
-
:authentication => nil,
-
:enable_starttls => nil,
-
:enable_starttls_auto => true,
-
:openssl_verify_mode => nil,
-
:ssl => nil,
-
:tls => nil,
-
:open_timeout => nil,
-
:read_timeout => nil
-
}
-
-
1
def initialize(values)
-
self.settings = DEFAULTS.merge(values)
-
end
-
-
1
def deliver!(mail)
-
response = start_smtp_session do |smtp|
-
Mail::SMTPConnection.new(:connection => smtp, :return_response => true).deliver!(mail)
-
end
-
-
settings[:return_response] ? response : self
-
end
-
-
1
private
-
1
def start_smtp_session(&block)
-
build_smtp_session.start(settings[:domain], settings[:user_name], settings[:password], settings[:authentication], &block)
-
end
-
-
1
def build_smtp_session
-
Net::SMTP.new(settings[:address], settings[:port]).tap do |smtp|
-
if settings[:tls] || settings[:ssl]
-
if smtp.respond_to?(:enable_tls)
-
smtp.enable_tls(ssl_context)
-
end
-
elsif settings[:enable_starttls]
-
if smtp.respond_to?(:enable_starttls)
-
smtp.enable_starttls(ssl_context)
-
end
-
elsif settings[:enable_starttls_auto]
-
if smtp.respond_to?(:enable_starttls_auto)
-
smtp.enable_starttls_auto(ssl_context)
-
end
-
end
-
-
smtp.open_timeout = settings[:open_timeout] if settings[:open_timeout]
-
smtp.read_timeout = settings[:read_timeout] if settings[:read_timeout]
-
end
-
end
-
-
# Allow SSL context to be configured via settings, for Ruby >= 1.9
-
# Just returns openssl verify mode for Ruby 1.8.x
-
1
def ssl_context
-
openssl_verify_mode = settings[:openssl_verify_mode]
-
-
if openssl_verify_mode.kind_of?(String)
-
openssl_verify_mode = OpenSSL::SSL.const_get("VERIFY_#{openssl_verify_mode.upcase}")
-
end
-
-
context = Net::SMTP.default_ssl_context
-
context.verify_mode = openssl_verify_mode if openssl_verify_mode
-
context.ca_path = settings[:ca_path] if settings[:ca_path]
-
context.ca_file = settings[:ca_file] if settings[:ca_file]
-
context
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'mail/check_delivery_params'
-
-
1
module Mail
-
# The TestMailer is a bare bones mailer that does nothing. It is useful
-
# when you are testing.
-
#
-
# It also provides a template of the minimum methods you require to implement
-
# if you want to make a custom mailer for Mail
-
1
class TestMailer
-
# Provides a store of all the emails sent with the TestMailer so you can check them.
-
1
def self.deliveries
-
@@deliveries ||= []
-
end
-
-
# Allows you to over write the default deliveries store from an array to some
-
# other object. If you just want to clear the store,
-
# call TestMailer.deliveries.clear.
-
#
-
# If you place another object here, please make sure it responds to:
-
#
-
# * << (message)
-
# * clear
-
# * length
-
# * size
-
# * and other common Array methods
-
1
def self.deliveries=(val)
-
@@deliveries = val
-
end
-
-
1
attr_accessor :settings
-
-
1
def initialize(values)
-
@settings = values.dup
-
end
-
-
1
def deliver!(mail)
-
Mail::CheckDeliveryParams.check(mail)
-
Mail::TestMailer.deliveries << mail
-
end
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
-
1
module Mail
-
-
1
class Retriever
-
-
# Get the oldest received email(s)
-
#
-
# Possible options:
-
# count: number of emails to retrieve. The default value is 1.
-
# order: order of emails returned. Possible values are :asc or :desc. Default value is :asc.
-
#
-
1
def first(options = {}, &block)
-
options ||= {}
-
options[:what] = :first
-
options[:count] ||= 1
-
find(options, &block)
-
end
-
-
# Get the most recent received email(s)
-
#
-
# Possible options:
-
# count: number of emails to retrieve. The default value is 1.
-
# order: order of emails returned. Possible values are :asc or :desc. Default value is :asc.
-
#
-
1
def last(options = {}, &block)
-
options ||= {}
-
options[:what] = :last
-
options[:count] ||= 1
-
find(options, &block)
-
end
-
-
# Get all emails.
-
#
-
# Possible options:
-
# order: order of emails returned. Possible values are :asc or :desc. Default value is :asc.
-
#
-
1
def all(options = {}, &block)
-
options ||= {}
-
options[:count] = :all
-
find(options, &block)
-
end
-
-
# Find emails in the mailbox, and then deletes them. Without any options, the
-
# five last received emails are returned.
-
#
-
# Possible options:
-
# what: last or first emails. The default is :first.
-
# order: order of emails returned. Possible values are :asc or :desc. Default value is :asc.
-
# count: number of emails to retrieve. The default value is 10. A value of 1 returns an
-
# instance of Message, not an array of Message instances.
-
# delete_after_find: flag for whether to delete each retreived email after find. Default
-
# is true. Call #find if you would like this to default to false.
-
#
-
1
def find_and_delete(options = {}, &block)
-
options ||= {}
-
options[:delete_after_find] ||= true
-
find(options, &block)
-
end
-
-
end
-
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
1
module Mail
-
1
class Part < Message
-
-
# Creates a new empty Content-ID field and inserts it in the correct order
-
# into the Header. The ContentIdField object will automatically generate
-
# a unique content ID if you try and encode it or output it to_s without
-
# specifying a content id.
-
#
-
# It will preserve the content ID you specify if you do.
-
1
def add_content_id(content_id_val = '')
-
header['content-id'] = content_id_val
-
end
-
-
# Returns true if the part has a content ID field, the field may or may
-
# not have a value, but the field exists or not.
-
1
def has_content_id?
-
header.has_content_id?
-
end
-
-
1
def inline_content_id
-
# TODO: Deprecated in 2.2.2 - Remove in 2.3
-
warn("Part#inline_content_id is deprecated, please call Part#cid instead")
-
cid
-
end
-
-
1
def cid
-
add_content_id unless has_content_id?
-
uri_escape(unbracket(content_id))
-
end
-
-
1
def url
-
"cid:#{cid}"
-
end
-
-
1
def inline?
-
header[:content_disposition].disposition_type == 'inline' if header[:content_disposition].respond_to?(:disposition_type)
-
end
-
-
1
def add_required_fields
-
super
-
add_content_id if !has_content_id? && inline?
-
end
-
-
1
def add_required_message_fields
-
# Override so we don't add Date, MIME-Version, or Message-ID.
-
end
-
-
1
def delivery_status_report_part?
-
(main_type =~ /message/i && sub_type =~ /delivery-status/i) && body =~ /Status:/
-
end
-
-
1
def delivery_status_data
-
delivery_status_report_part? ? parse_delivery_status_report : {}
-
end
-
-
1
def bounced?
-
if action.is_a?(Array)
-
!!(action.first =~ /failed/i)
-
else
-
!!(action =~ /failed/i)
-
end
-
end
-
-
-
# Either returns the action if the message has just a single report, or an
-
# array of all the actions, one for each report
-
1
def action
-
get_return_values('action')
-
end
-
-
1
def final_recipient
-
get_return_values('final-recipient')
-
end
-
-
1
def error_status
-
get_return_values('status')
-
end
-
-
1
def diagnostic_code
-
get_return_values('diagnostic-code')
-
end
-
-
1
def remote_mta
-
get_return_values('remote-mta')
-
end
-
-
1
def retryable?
-
!(error_status =~ /^5/)
-
end
-
-
1
private
-
-
1
def get_return_values(key)
-
if delivery_status_data[key].is_a?(Array)
-
delivery_status_data[key].map { |a| a.value }
-
elsif !delivery_status_data[key].nil?
-
delivery_status_data[key].value
-
else
-
nil
-
end
-
end
-
-
# A part may not have a header.... so, just init a body if no header
-
1
def parse_message
-
header_part, body_part = raw_source.split(/#{Constants::CRLF}#{Constants::WSP}*#{Constants::CRLF}/m, 2)
-
if header_part =~ Constants::HEADER_LINE
-
self.header = header_part
-
self.body = body_part
-
else
-
self.header = "Content-Type: text/plain\r\n"
-
self.body = raw_source
-
end
-
end
-
-
1
def parse_delivery_status_report
-
@delivery_status_data ||= Header.new(body.to_s.gsub("\r\n\r\n", "\r\n"))
-
end
-
-
end
-
-
end
-
# frozen_string_literal: true
-
1
require 'delegate'
-
-
1
module Mail
-
1
class PartsList < DelegateClass(Array)
-
1
attr_reader :parts
-
-
1
def initialize(*args)
-
@parts = Array.new(*args)
-
super @parts
-
end
-
-
# The #encode_with and #to_yaml methods are just implemented
-
# for the sake of backward compatibility ; the delegator does
-
# not correctly delegate these calls to the delegated object
-
1
def encode_with(coder) # :nodoc:
-
coder.represent_object(nil, @parts)
-
end
-
-
1
def to_yaml(options = {}) # :nodoc:
-
@parts.to_yaml(options)
-
end
-
-
1
def attachments
-
Mail::AttachmentsList.new(@parts)
-
end
-
-
1
def collect
-
if block_given?
-
ary = PartsList.new
-
each { |o| ary << yield(o) }
-
ary
-
else
-
to_a
-
end
-
end
-
1
alias_method :map, :collect
-
-
1
def map!
-
raise NoMethodError, "#map! is not defined, please call #collect and create a new PartsList"
-
end
-
-
1
def collect!
-
raise NoMethodError, "#collect! is not defined, please call #collect and create a new PartsList"
-
end
-
-
1
def sort
-
self.class.new(@parts.sort)
-
end
-
-
1
def sort!(order)
-
# stable sort should be used to maintain the relative order as the parts are added
-
i = 0;
-
sorted = @parts.sort_by do |a|
-
# OK, 10000 is arbitrary... if anyone actually wants to explicitly sort 10000 parts of a
-
# single email message... please show me a use case and I'll put more work into this method,
-
# in the meantime, it works :)
-
get_order_value(a, order) << (i += 1)
-
end
-
@parts.clear
-
sorted.each { |p| @parts << p }
-
end
-
-
1
private
-
-
1
def get_order_value(part, order)
-
is_attachment = part.respond_to?(:attachment?) && part.attachment?
-
has_content_type = part.respond_to?(:content_type) && !part[:content_type].nil?
-
-
[is_attachment ? 1 : 0, (has_content_type ? order.index(part[:content_type].string.downcase) : nil) || 10000]
-
end
-
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
1
require 'mail/constants'
-
-
1
module Mail
-
1
module Utilities
-
-
1
LF = "\n"
-
1
CRLF = "\r\n"
-
-
1
include Constants
-
-
# Returns true if the string supplied is free from characters not allowed as an ATOM
-
1
def atom_safe?( str )
-
not ATOM_UNSAFE === str
-
end
-
-
# If the string supplied has ATOM unsafe characters in it, will return the string quoted
-
# in double quotes, otherwise returns the string unmodified
-
1
def quote_atom( str )
-
atom_safe?( str ) ? str : dquote(str)
-
end
-
-
# If the string supplied has PHRASE unsafe characters in it, will return the string quoted
-
# in double quotes, otherwise returns the string unmodified
-
1
def quote_phrase( str )
-
if str.respond_to?(:force_encoding)
-
original_encoding = str.encoding
-
ascii_str = str.to_s.dup.force_encoding('ASCII-8BIT')
-
if (PHRASE_UNSAFE === ascii_str)
-
dquote(ascii_str).force_encoding(original_encoding)
-
else
-
str
-
end
-
else
-
(PHRASE_UNSAFE === str) ? dquote(str) : str
-
end
-
end
-
-
# Returns true if the string supplied is free from characters not allowed as a TOKEN
-
1
def token_safe?( str )
-
not TOKEN_UNSAFE === str
-
end
-
-
# If the string supplied has TOKEN unsafe characters in it, will return the string quoted
-
# in double quotes, otherwise returns the string unmodified
-
1
def quote_token( str )
-
if str.respond_to?(:force_encoding)
-
original_encoding = str.encoding
-
ascii_str = str.to_s.dup.force_encoding('ASCII-8BIT')
-
if token_safe?( ascii_str )
-
str
-
else
-
dquote(ascii_str).force_encoding(original_encoding)
-
end
-
else
-
token_safe?( str ) ? str : dquote(str)
-
end
-
end
-
-
# Wraps supplied string in double quotes and applies \-escaping as necessary,
-
# unless it is already wrapped.
-
#
-
# Example:
-
#
-
# string = 'This is a string'
-
# dquote(string) #=> '"This is a string"'
-
#
-
# string = 'This is "a string"'
-
# dquote(string #=> '"This is \"a string\"'
-
1
def dquote( str )
-
'"' + unquote(str).gsub(/[\\"]/n) {|s| '\\' + s } + '"'
-
end
-
-
# Unwraps supplied string from inside double quotes and
-
# removes any \-escaping.
-
#
-
# Example:
-
#
-
# string = '"This is a string"'
-
# unquote(string) #=> 'This is a string'
-
#
-
# string = '"This is \"a string\""'
-
# unqoute(string) #=> 'This is "a string"'
-
1
def unquote( str )
-
if str =~ /^"(.*?)"$/
-
unescape($1)
-
else
-
str
-
end
-
end
-
1
module_function :unquote
-
-
# Removes any \-escaping.
-
#
-
# Example:
-
#
-
# string = 'This is \"a string\"'
-
# unescape(string) #=> 'This is "a string"'
-
#
-
# string = '"This is \"a string\""'
-
# unescape(string) #=> '"This is "a string""'
-
1
def unescape( str )
-
str.gsub(/\\(.)/, '\1')
-
end
-
1
module_function :unescape
-
-
# Wraps a string in parenthesis and escapes any that are in the string itself.
-
#
-
# Example:
-
#
-
# paren( 'This is a string' ) #=> '(This is a string)'
-
1
def paren( str )
-
RubyVer.paren( str )
-
end
-
-
# Unwraps a string from being wrapped in parenthesis
-
#
-
# Example:
-
#
-
# str = '(This is a string)'
-
# unparen( str ) #=> 'This is a string'
-
1
def unparen( str )
-
match = str.match(/^\((.*?)\)$/)
-
match ? match[1] : str
-
end
-
-
# Wraps a string in angle brackets and escapes any that are in the string itself
-
#
-
# Example:
-
#
-
# bracket( 'This is a string' ) #=> '<This is a string>'
-
1
def bracket( str )
-
RubyVer.bracket( str )
-
end
-
-
# Unwraps a string from being wrapped in parenthesis
-
#
-
# Example:
-
#
-
# str = '<This is a string>'
-
# unbracket( str ) #=> 'This is a string'
-
1
def unbracket( str )
-
match = str.match(/^\<(.*?)\>$/)
-
match ? match[1] : str
-
end
-
-
# Escape parenthesies in a string
-
#
-
# Example:
-
#
-
# str = 'This is (a) string'
-
# escape_paren( str ) #=> 'This is \(a\) string'
-
1
def escape_paren( str )
-
RubyVer.escape_paren( str )
-
end
-
-
1
def uri_escape( str )
-
uri_parser.escape(str)
-
end
-
-
1
def uri_unescape( str )
-
uri_parser.unescape(str)
-
end
-
-
1
def uri_parser
-
@uri_parser ||= URI.const_defined?(:Parser) ? URI::Parser.new : URI
-
end
-
-
# Matches two objects with their to_s values case insensitively
-
#
-
# Example:
-
#
-
# obj2 = "This_is_An_object"
-
# obj1 = :this_IS_an_object
-
# match_to_s( obj1, obj2 ) #=> true
-
1
def match_to_s( obj1, obj2 )
-
obj1.to_s.casecmp(obj2.to_s) == 0
-
end
-
-
# Capitalizes a string that is joined by hyphens correctly.
-
#
-
# Example:
-
#
-
# string = 'resent-from-field'
-
# capitalize_field( string ) #=> 'Resent-From-Field'
-
1
def capitalize_field( str )
-
str.to_s.split("-").map { |v| v.capitalize }.join("-")
-
end
-
-
# Takes an underscored word and turns it into a class name
-
#
-
# Example:
-
#
-
# constantize("hello") #=> "Hello"
-
# constantize("hello-there") #=> "HelloThere"
-
# constantize("hello-there-mate") #=> "HelloThereMate"
-
1
def constantize( str )
-
str.to_s.split(/[-_]/).map { |v| v.capitalize }.to_s
-
end
-
-
# Swaps out all underscores (_) for hyphens (-) good for stringing from symbols
-
# a field name.
-
#
-
# Example:
-
#
-
# string = :resent_from_field
-
# dasherize( string ) #=> 'resent-from-field'
-
1
def dasherize( str )
-
str.to_s.tr(UNDERSCORE, HYPHEN)
-
end
-
-
# Swaps out all hyphens (-) for underscores (_) good for stringing to symbols
-
# a field name.
-
#
-
# Example:
-
#
-
# string = :resent_from_field
-
# underscoreize ( string ) #=> 'resent_from_field'
-
1
def underscoreize( str )
-
8
str.to_s.downcase.tr(HYPHEN, UNDERSCORE)
-
end
-
-
1
if RUBY_VERSION <= '1.8.6'
-
-
def map_lines( str, &block )
-
results = []
-
str.each_line do |line|
-
results << yield(line)
-
end
-
results
-
end
-
-
def map_with_index( enum, &block )
-
results = []
-
enum.each_with_index do |token, i|
-
results[i] = yield(token, i)
-
end
-
results
-
end
-
-
else
-
-
1
def map_lines( str, &block )
-
str.each_line.map(&block)
-
end
-
-
1
def map_with_index( enum, &block )
-
enum.each_with_index.map(&block)
-
end
-
-
end
-
-
1
def self.binary_unsafe_to_lf(string) #:nodoc:
-
string.gsub(/\r\n|\r/, LF)
-
end
-
-
TO_CRLF_REGEX =
-
if RUBY_VERSION >= '1.9'
-
# This 1.9 only regex can save a reasonable amount of time (~20%)
-
# by not matching "\r\n" so the string is returned unchanged in
-
# the common case.
-
1
Regexp.new("(?<!\r)\n|\r(?!\n)")
-
else
-
/\n|\r\n|\r/
-
end
-
-
1
def self.binary_unsafe_to_crlf(string) #:nodoc:
-
string.gsub(TO_CRLF_REGEX, CRLF)
-
end
-
-
1
if RUBY_VERSION < '1.9'
-
def self.safe_for_line_ending_conversion?(string) #:nodoc:
-
string.ascii_only?
-
end
-
else
-
1
def self.safe_for_line_ending_conversion?(string) #:nodoc:
-
if string.encoding == Encoding::BINARY
-
string.ascii_only?
-
else
-
string.valid_encoding?
-
end
-
end
-
end
-
-
# Convert line endings to \n unless the string is binary. Used for
-
# sendmail delivery and for decoding 8bit Content-Transfer-Encoding.
-
1
def self.to_lf(string)
-
string = string.to_s
-
if safe_for_line_ending_conversion? string
-
binary_unsafe_to_lf string
-
else
-
string
-
end
-
end
-
-
# Convert line endings to \r\n unless the string is binary. Used for
-
# encoding 8bit and base64 Content-Transfer-Encoding and for convenience
-
# when parsing emails with \n line endings instead of the required \r\n.
-
1
def self.to_crlf(string)
-
string = string.to_s
-
if safe_for_line_ending_conversion? string
-
binary_unsafe_to_crlf string
-
else
-
string
-
end
-
end
-
-
# Returns true if the object is considered blank.
-
# A blank includes things like '', ' ', nil,
-
# and arrays and hashes that have nothing in them.
-
#
-
# This logic is mostly shared with ActiveSupport's blank?
-
1
def self.blank?(value)
-
if value.kind_of?(NilClass)
-
true
-
elsif value.kind_of?(String)
-
value !~ /\S/
-
else
-
value.respond_to?(:empty?) ? value.empty? : !value
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
module Mail
-
1
module VERSION
-
-
1
MAJOR = 2
-
1
MINOR = 7
-
1
PATCH = 1
-
1
BUILD = nil
-
-
1
STRING = [MAJOR, MINOR, PATCH, BUILD].compact.join('.')
-
-
1
def self.version
-
STRING
-
end
-
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
-
1
module Mail
-
1
class Ruby19
-
1
class StrictCharsetEncoder
-
1
def encode(string, charset)
-
case charset
-
when /utf-?7/i
-
Mail::Ruby19.decode_utf7(string)
-
else
-
string.force_encoding(Mail::Ruby19.pick_encoding(charset))
-
end
-
end
-
end
-
-
1
class BestEffortCharsetEncoder
-
1
def encode(string, charset)
-
case charset
-
when /utf-?7/i
-
Mail::Ruby19.decode_utf7(string)
-
else
-
string.force_encoding(pick_encoding(charset))
-
end
-
end
-
-
1
private
-
-
1
def pick_encoding(charset)
-
charset = case charset
-
when /ansi_x3.110-1983/
-
'ISO-8859-1'
-
when /Windows-?1258/i # Windows-1258 is similar to 1252
-
"Windows-1252"
-
else
-
charset
-
end
-
Mail::Ruby19.pick_encoding(charset)
-
end
-
end
-
-
1
class << self
-
1
attr_accessor :charset_encoder
-
end
-
1
self.charset_encoder = BestEffortCharsetEncoder.new
-
-
# Escapes any parenthesis in a string that are unescaped this uses
-
# a Ruby 1.9.1 regexp feature of negative look behind
-
1
def Ruby19.escape_paren( str )
-
re = /(?<!\\)([\(\)])/ # Only match unescaped parens
-
str.gsub(re) { |s| '\\' + s }
-
end
-
-
1
def Ruby19.paren( str )
-
str = $1 if str =~ /^\((.*)?\)$/
-
str = escape_paren( str )
-
'(' + str + ')'
-
end
-
-
1
def Ruby19.escape_bracket( str )
-
re = /(?<!\\)([\<\>])/ # Only match unescaped brackets
-
str.gsub(re) { |s| '\\' + s }
-
end
-
-
1
def Ruby19.bracket( str )
-
str = $1 if str =~ /^\<(.*)?\>$/
-
str = escape_bracket( str )
-
'<' + str + '>'
-
end
-
-
1
def Ruby19.decode_base64(str)
-
if !str.end_with?("=") && str.length % 4 != 0
-
str = str.ljust((str.length + 3) & ~3, "=")
-
end
-
str.unpack( 'm' ).first
-
end
-
-
1
def Ruby19.encode_base64(str)
-
[str].pack( 'm' )
-
end
-
-
1
def Ruby19.has_constant?(klass, string)
-
klass.const_defined?( string, false )
-
end
-
-
1
def Ruby19.get_constant(klass, string)
-
klass.const_get( string )
-
end
-
-
1
def Ruby19.transcode_charset(str, from_encoding, to_encoding = Encoding::UTF_8)
-
to_encoding = to_encoding.to_s if RUBY_VERSION < '1.9.3'
-
to_encoding = Encoding.find(to_encoding)
-
replacement_char = to_encoding == Encoding::UTF_8 ? '���' : '?'
-
charset_encoder.encode(str.dup, from_encoding).encode(to_encoding, :undef => :replace, :invalid => :replace, :replace => replacement_char)
-
end
-
-
# From Ruby stdlib Net::IMAP
-
1
def Ruby19.encode_utf7(string)
-
string.gsub(/(&)|[^\x20-\x7e]+/) do
-
if $1
-
"&-"
-
else
-
base64 = [$&.encode(Encoding::UTF_16BE)].pack("m0")
-
"&" + base64.delete("=").tr("/", ",") + "-"
-
end
-
end.force_encoding(Encoding::ASCII_8BIT)
-
end
-
-
1
def Ruby19.decode_utf7(utf7)
-
utf7.gsub(/&([^-]+)?-/n) do
-
if $1
-
($1.tr(",", "/") + "===").unpack("m")[0].encode(Encoding::UTF_8, Encoding::UTF_16BE)
-
else
-
"&"
-
end
-
end
-
end
-
-
1
def Ruby19.b_value_encode(str, encoding = nil)
-
encoding = str.encoding.to_s
-
[Ruby19.encode_base64(str), encoding]
-
end
-
-
1
def Ruby19.b_value_decode(str)
-
match = str.match(/\=\?(.+)?\?[Bb]\?(.*)\?\=/m)
-
if match
-
charset = match[1]
-
str = Ruby19.decode_base64(match[2])
-
str = charset_encoder.encode(str, charset)
-
end
-
transcode_to_scrubbed_utf8(str)
-
rescue Encoding::UndefinedConversionError, ArgumentError, Encoding::ConverterNotFoundError
-
warn "Encoding conversion failed #{$!}"
-
str.dup.force_encoding(Encoding::UTF_8)
-
end
-
-
1
def Ruby19.q_value_encode(str, encoding = nil)
-
encoding = str.encoding.to_s
-
[Encodings::QuotedPrintable.encode(str), encoding]
-
end
-
-
1
def Ruby19.q_value_decode(str)
-
match = str.match(/\=\?(.+)?\?[Qq]\?(.*)\?\=/m)
-
if match
-
charset = match[1]
-
string = match[2].gsub(/_/, '=20')
-
# Remove trailing = if it exists in a Q encoding
-
string = string.sub(/\=$/, '')
-
str = Encodings::QuotedPrintable.decode(string)
-
str = charset_encoder.encode(str, charset)
-
# We assume that binary strings hold utf-8 directly to work around
-
# jruby/jruby#829 which subtly changes String#encode semantics.
-
str.force_encoding(Encoding::UTF_8) if str.encoding == Encoding::ASCII_8BIT
-
end
-
transcode_to_scrubbed_utf8(str)
-
rescue Encoding::UndefinedConversionError, ArgumentError, Encoding::ConverterNotFoundError
-
warn "Encoding conversion failed #{$!}"
-
str.dup.force_encoding(Encoding::UTF_8)
-
end
-
-
1
def Ruby19.param_decode(str, encoding)
-
str = uri_parser.unescape(str)
-
str = charset_encoder.encode(str, encoding) if encoding
-
transcode_to_scrubbed_utf8(str)
-
rescue Encoding::UndefinedConversionError, ArgumentError, Encoding::ConverterNotFoundError
-
warn "Encoding conversion failed #{$!}"
-
str.dup.force_encoding(Encoding::UTF_8)
-
end
-
-
1
def Ruby19.param_encode(str)
-
encoding = str.encoding.to_s.downcase
-
language = Configuration.instance.param_encode_language
-
"#{encoding}'#{language}'#{uri_parser.escape(str)}"
-
end
-
-
1
def Ruby19.uri_parser
-
@uri_parser ||= URI::Parser.new
-
end
-
-
# Pick a Ruby encoding corresponding to the message charset. Most
-
# charsets have a Ruby encoding, but some need manual aliasing here.
-
#
-
# TODO: add this as a test somewhere:
-
# Encoding.list.map { |e| [e.to_s.upcase == pick_encoding(e.to_s.downcase.gsub("-", "")), e.to_s] }.select {|a,b| !b}
-
# Encoding.list.map { |e| [e.to_s == pick_encoding(e.to_s), e.to_s] }.select {|a,b| !b}
-
1
def Ruby19.pick_encoding(charset)
-
charset = charset.to_s
-
encoding = case charset.downcase
-
-
# ISO-8859-8-I etc. http://en.wikipedia.org/wiki/ISO-8859-8-I
-
when /^iso[-_]?8859-(\d+)(-i)?$/
-
"ISO-8859-#{$1}"
-
-
# ISO-8859-15, ISO-2022-JP and alike
-
when /^iso[-_]?(\d{4})-?(\w{1,2})$/
-
"ISO-#{$1}-#{$2}"
-
-
# "ISO-2022-JP-KDDI" and alike
-
when /^iso[-_]?(\d{4})-?(\w{1,2})-?(\w*)$/
-
"ISO-#{$1}-#{$2}-#{$3}"
-
-
# UTF-8, UTF-32BE and alike
-
when /^utf[\-_]?(\d{1,2})?(\w{1,2})$/
-
"UTF-#{$1}#{$2}".gsub(/\A(UTF-(?:16|32))\z/, '\\1BE')
-
-
# Windows-1252 and alike
-
when /^windows-?(.*)$/
-
"Windows-#{$1}"
-
-
when '8bit'
-
Encoding::ASCII_8BIT
-
-
# alternatives/misspellings of us-ascii seen in the wild
-
when /^iso[-_]?646(-us)?$/, 'us=ascii'
-
Encoding::ASCII
-
-
# Microsoft-specific alias for MACROMAN
-
when 'macintosh'
-
Encoding::MACROMAN
-
-
# Microsoft-specific alias for CP949 (Korean)
-
when 'ks_c_5601-1987'
-
Encoding::CP949
-
-
# Wrongly written Shift_JIS (Japanese)
-
when 'shift-jis'
-
Encoding::Shift_JIS
-
-
# GB2312 (Chinese charset) is a subset of GB18030 (its replacement)
-
when 'gb2312'
-
Encoding::GB18030
-
-
when 'cp-850'
-
Encoding::CP850
-
-
when 'latin2'
-
Encoding::ISO_8859_2
-
-
else
-
charset
-
end
-
-
convert_to_encoding(encoding)
-
end
-
-
1
if "string".respond_to?(:byteslice)
-
1
def Ruby19.string_byteslice(str, *args)
-
str.byteslice(*args)
-
end
-
else
-
def Ruby19.string_byteslice(str, *args)
-
str.unpack('C*').slice(*args).pack('C*').force_encoding(str.encoding)
-
end
-
end
-
-
1
class << self
-
1
private
-
-
1
def convert_to_encoding(encoding)
-
if encoding.is_a?(Encoding)
-
encoding
-
else
-
# Fall back to ASCII for charsets that Ruby doesn't recognize
-
begin
-
Encoding.find(encoding)
-
rescue ArgumentError
-
Encoding::BINARY
-
end
-
end
-
end
-
-
1
def transcode_to_scrubbed_utf8(str)
-
decoded = str.encode(Encoding::UTF_8, :undef => :replace, :invalid => :replace, :replace => "���")
-
decoded.valid_encoding? ? decoded : decoded.encode(Encoding::UTF_16LE, :invalid => :replace, :replace => "���").encode(Encoding::UTF_8)
-
end
-
end
-
end
-
end
-
1
require "mini_mime/version"
-
1
require "thread"
-
-
1
module MiniMime
-
1
def self.lookup_by_filename(filename)
-
Db.lookup_by_filename(filename)
-
end
-
-
1
def self.lookup_by_extension(extension)
-
Db.lookup_by_extension(extension)
-
end
-
-
1
def self.lookup_by_content_type(mime)
-
Db.lookup_by_content_type(mime)
-
end
-
-
1
module Configuration
-
1
class << self
-
1
attr_accessor :ext_db_path
-
1
attr_accessor :content_type_db_path
-
end
-
-
1
self.ext_db_path = File.expand_path("../db/ext_mime.db", __FILE__)
-
1
self.content_type_db_path = File.expand_path("../db/content_type_mime.db", __FILE__)
-
end
-
-
1
class Info
-
1
BINARY_ENCODINGS = %w(base64 8bit)
-
-
1
attr_accessor :extension, :content_type, :encoding
-
-
1
def initialize(buffer)
-
@extension, @content_type, @encoding = buffer.split(/\s+/).map!(&:freeze)
-
end
-
-
1
def [](idx)
-
if idx == 0
-
@extension
-
elsif idx == 1
-
@content_type
-
elsif idx == 2
-
@encoding
-
end
-
end
-
-
1
def binary?
-
BINARY_ENCODINGS.include?(encoding)
-
end
-
end
-
-
1
class Db
-
1
LOCK = Mutex.new
-
-
1
def self.lookup_by_filename(filename)
-
extension = File.extname(filename)
-
return if extension.empty?
-
extension = extension[1..-1]
-
extension.downcase!
-
lookup_by_extension(extension)
-
end
-
-
1
def self.lookup_by_extension(extension)
-
LOCK.synchronize do
-
@db ||= new
-
@db.lookup_by_extension(extension)
-
end
-
end
-
-
1
def self.lookup_by_content_type(content_type)
-
LOCK.synchronize do
-
@db ||= new
-
@db.lookup_by_content_type(content_type)
-
end
-
end
-
-
1
class Cache
-
1
def initialize(size)
-
@size = size
-
@hash = {}
-
end
-
-
1
def []=(key, val)
-
rval = @hash[key] = val
-
@hash.shift if @hash.length > @size
-
rval
-
end
-
-
1
def fetch(key, &blk)
-
@hash.fetch(key, &blk)
-
end
-
end
-
-
1
class RandomAccessDb
-
1
MAX_CACHED = 100
-
-
1
def initialize(path, sort_order)
-
@path = path
-
@file = File.open(@path)
-
-
@row_length = @file.readline.length
-
@file_length = File.size(@path)
-
@rows = @file_length / @row_length
-
-
@hit_cache = Cache.new(MAX_CACHED)
-
@miss_cache = Cache.new(MAX_CACHED)
-
-
@sort_order = sort_order
-
end
-
-
1
def lookup(val)
-
@hit_cache.fetch(val) do
-
@miss_cache.fetch(val) do
-
data = lookup_uncached(val)
-
if data
-
@hit_cache[val] = data
-
else
-
@miss_cache[val] = nil
-
end
-
-
data
-
end
-
end
-
end
-
-
# lifted from marcandre/backports
-
1
def lookup_uncached(val)
-
from = 0
-
to = @rows - 1
-
result = nil
-
-
while from <= to do
-
midpoint = from + (to-from).div(2)
-
current = resolve(midpoint)
-
data = current[@sort_order]
-
if data > val
-
to = midpoint - 1
-
elsif data < val
-
from = midpoint + 1
-
else
-
result = current
-
break
-
end
-
end
-
result
-
end
-
-
1
def resolve(row)
-
@file.seek(row*@row_length)
-
Info.new(@file.readline)
-
end
-
end
-
-
1
def initialize
-
@ext_db = RandomAccessDb.new(Configuration.ext_db_path, 0)
-
@content_type_db = RandomAccessDb.new(Configuration.content_type_db_path, 1)
-
end
-
-
1
def lookup_by_extension(extension)
-
@ext_db.lookup(extension)
-
end
-
-
1
def lookup_by_content_type(content_type)
-
@content_type_db.lookup(content_type)
-
end
-
end
-
end
-
1
module MiniMime
-
1
VERSION = "1.0.2"
-
end
-
1
require "minitest"
-
-
1
module Minitest
-
1
def self.plugin_pride_options opts, _options # :nodoc:
-
1
opts.on "-p", "--pride", "Pride. Show your testing pride!" do
-
PrideIO.pride!
-
end
-
end
-
-
1
def self.plugin_pride_init options # :nodoc:
-
1
if PrideIO.pride? then
-
klass = ENV["TERM"] =~ /^xterm|-256color$/ ? PrideLOL : PrideIO
-
io = klass.new options[:io]
-
-
self.reporter.reporters.grep(Minitest::Reporter).each do |rep|
-
rep.io = io if rep.io.tty?
-
end
-
end
-
end
-
-
##
-
# Show your testing pride!
-
-
1
class PrideIO
-
##
-
# Activate the pride plugin. Called from both -p option and minitest/pride
-
-
1
def self.pride!
-
@pride = true
-
end
-
-
##
-
# Are we showing our testing pride?
-
-
1
def self.pride?
-
1
@pride ||= false
-
end
-
-
# Start an escape sequence
-
1
ESC = "\e["
-
-
# End the escape sequence
-
1
NND = "#{ESC}0m"
-
-
# The IO we're going to pipe through.
-
1
attr_reader :io
-
-
1
def initialize io # :nodoc:
-
@io = io
-
# stolen from /System/Library/Perl/5.10.0/Term/ANSIColor.pm
-
# also reference http://en.wikipedia.org/wiki/ANSI_escape_code
-
@colors ||= (31..36).to_a
-
@size = @colors.size
-
@index = 0
-
end
-
-
##
-
# Wrap print to colorize the output.
-
-
1
def print o
-
case o
-
when "." then
-
io.print pride o
-
when "E", "F" then
-
io.print "#{ESC}41m#{ESC}37m#{o}#{NND}"
-
when "S" then
-
io.print pride o
-
else
-
io.print o
-
end
-
end
-
-
1
def puts *o # :nodoc:
-
o.map! { |s|
-
s.to_s.sub(/Finished/) {
-
@index = 0
-
"Fabulous run".split(//).map { |c|
-
pride(c)
-
}.join
-
}
-
}
-
-
io.puts(*o)
-
end
-
-
##
-
# Color a string.
-
-
1
def pride string
-
string = "*" if string == "."
-
c = @colors[@index % @size]
-
@index += 1
-
"#{ESC}#{c}m#{string}#{NND}"
-
end
-
-
1
def method_missing msg, *args # :nodoc:
-
io.send(msg, *args)
-
end
-
end
-
-
##
-
# If you thought the PrideIO was colorful...
-
#
-
# (Inspired by lolcat, but with clean math)
-
-
1
class PrideLOL < PrideIO
-
1
PI_3 = Math::PI / 3 # :nodoc:
-
-
1
def initialize io # :nodoc:
-
# walk red, green, and blue around a circle separated by equal thirds.
-
#
-
# To visualize, type this into wolfram-alpha:
-
#
-
# plot (3*sin(x)+3), (3*sin(x+2*pi/3)+3), (3*sin(x+4*pi/3)+3)
-
-
# 6 has wide pretty gradients. 3 == lolcat, about half the width
-
@colors = (0...(6 * 7)).map { |n|
-
n *= 1.0 / 6
-
r = (3 * Math.sin(n ) + 3).to_i
-
g = (3 * Math.sin(n + 2 * PI_3) + 3).to_i
-
b = (3 * Math.sin(n + 4 * PI_3) + 3).to_i
-
-
# Then we take rgb and encode them in a single number using base 6.
-
# For some mysterious reason, we add 16... to clear the bottom 4 bits?
-
# Yes... they're ugly.
-
-
36 * r + 6 * g + b + 16
-
}
-
-
super
-
end
-
-
##
-
# Make the string even more colorful. Damnit.
-
-
1
def pride string
-
c = @colors[@index % @size]
-
@index += 1
-
"#{ESC}38;5;#{c}m#{string}#{NND}"
-
end
-
end
-
end
-
1
require 'rack/utils'
-
-
1
module Rack
-
-
# Middleware that enables conditional GET using If-None-Match and
-
# If-Modified-Since. The application should set either or both of the
-
# Last-Modified or Etag response headers according to RFC 2616. When
-
# either of the conditions is met, the response body is set to be zero
-
# length and the response status is set to 304 Not Modified.
-
#
-
# Applications that defer response body generation until the body's each
-
# message is received will avoid response body generation completely when
-
# a conditional GET matches.
-
#
-
# Adapted from Michael Klishin's Merb implementation:
-
# https://github.com/wycats/merb/blob/master/merb-core/lib/merb-core/rack/middleware/conditional_get.rb
-
1
class ConditionalGet
-
1
def initialize(app)
-
1
@app = app
-
end
-
-
1
def call(env)
-
2
case env[REQUEST_METHOD]
-
when "GET", "HEAD"
-
2
status, headers, body = @app.call(env)
-
2
headers = Utils::HeaderHash.new(headers)
-
2
if status == 200 && fresh?(env, headers)
-
status = 304
-
headers.delete(CONTENT_TYPE)
-
headers.delete(CONTENT_LENGTH)
-
original_body = body
-
body = Rack::BodyProxy.new([]) do
-
original_body.close if original_body.respond_to?(:close)
-
end
-
end
-
2
[status, headers, body]
-
else
-
@app.call(env)
-
end
-
end
-
-
1
private
-
-
1
def fresh?(env, headers)
-
2
modified_since = env['HTTP_IF_MODIFIED_SINCE']
-
2
none_match = env['HTTP_IF_NONE_MATCH']
-
-
2
return false unless modified_since || none_match
-
-
success = true
-
success &&= modified_since?(to_rfc2822(modified_since), headers) if modified_since
-
success &&= etag_matches?(none_match, headers) if none_match
-
success
-
end
-
-
1
def etag_matches?(none_match, headers)
-
etag = headers['ETag'] and etag == none_match
-
end
-
-
1
def modified_since?(modified_since, headers)
-
last_modified = to_rfc2822(headers['Last-Modified']) and
-
modified_since and
-
modified_since >= last_modified
-
end
-
-
1
def to_rfc2822(since)
-
# shortest possible valid date is the obsolete: 1 Nov 97 09:55 A
-
# anything shorter is invalid, this avoids exceptions for common cases
-
# most common being the empty string
-
if since && since.length >= 16
-
# NOTE: there is no trivial way to write this in a non execption way
-
# _rfc2822 returns a hash but is not that usable
-
Time.rfc2822(since) rescue nil
-
else
-
nil
-
end
-
end
-
end
-
end
-
1
require 'rack'
-
1
require 'digest/sha2'
-
-
1
module Rack
-
# Automatically sets the ETag header on all String bodies.
-
#
-
# The ETag header is skipped if ETag or Last-Modified headers are sent or if
-
# a sendfile body (body.responds_to :to_path) is given (since such cases
-
# should be handled by apache/nginx).
-
#
-
# On initialization, you can pass two parameters: a Cache-Control directive
-
# used when Etag is absent and a directive when it is present. The first
-
# defaults to nil, while the second defaults to "max-age=0, private, must-revalidate"
-
1
class ETag
-
1
ETAG_STRING = Rack::ETAG
-
1
DEFAULT_CACHE_CONTROL = "max-age=0, private, must-revalidate".freeze
-
-
1
def initialize(app, no_cache_control = nil, cache_control = DEFAULT_CACHE_CONTROL)
-
1
@app = app
-
1
@cache_control = cache_control
-
1
@no_cache_control = no_cache_control
-
end
-
-
1
def call(env)
-
2
status, headers, body = @app.call(env)
-
-
2
if etag_status?(status) && etag_body?(body) && !skip_caching?(headers)
-
2
original_body = body
-
2
digest, new_body = digest_body(body)
-
2
body = Rack::BodyProxy.new(new_body) do
-
2
original_body.close if original_body.respond_to?(:close)
-
end
-
2
headers[ETAG_STRING] = %(W/"#{digest}") if digest
-
end
-
-
2
unless headers[CACHE_CONTROL]
-
2
if digest
-
2
headers[CACHE_CONTROL] = @cache_control if @cache_control
-
else
-
headers[CACHE_CONTROL] = @no_cache_control if @no_cache_control
-
end
-
end
-
-
2
[status, headers, body]
-
end
-
-
1
private
-
-
1
def etag_status?(status)
-
2
status == 200 || status == 201
-
end
-
-
1
def etag_body?(body)
-
2
!body.respond_to?(:to_path)
-
end
-
-
1
def skip_caching?(headers)
-
2
(headers[CACHE_CONTROL] && headers[CACHE_CONTROL].include?('no-cache')) ||
-
1
headers.key?(ETAG_STRING) || headers.key?('Last-Modified')
-
end
-
-
1
def digest_body(body)
-
2
parts = []
-
2
digest = nil
-
-
2
body.each do |part|
-
2
parts << part
-
2
(digest ||= Digest::SHA256.new) << part unless part.empty?
-
end
-
-
2
[digest && digest.hexdigest.byteslice(0, 32), parts]
-
end
-
end
-
end
-
1
require 'time'
-
1
require 'rack/utils'
-
1
require 'rack/mime'
-
1
require 'rack/request'
-
1
require 'rack/head'
-
-
1
module Rack
-
# Rack::File serves files below the +root+ directory given, according to the
-
# path info of the Rack request.
-
# e.g. when Rack::File.new("/etc") is used, you can access 'passwd' file
-
# as http://localhost:9292/passwd
-
#
-
# Handlers can detect if bodies are a Rack::File, and use mechanisms
-
# like sendfile on the +path+.
-
-
1
class File
-
1
ALLOWED_VERBS = %w[GET HEAD OPTIONS]
-
1
ALLOW_HEADER = ALLOWED_VERBS.join(', ')
-
-
1
attr_reader :root
-
-
1
def initialize(root, headers={}, default_mime = 'text/plain')
-
1
@root = root
-
1
@headers = headers
-
1
@default_mime = default_mime
-
1
@head = Rack::Head.new(lambda { |env| get env })
-
end
-
-
1
def call(env)
-
# HEAD requests drop the response body, including 4xx error messages.
-
@head.call env
-
end
-
-
1
def get(env)
-
request = Rack::Request.new env
-
unless ALLOWED_VERBS.include? request.request_method
-
return fail(405, "Method Not Allowed", {'Allow' => ALLOW_HEADER})
-
end
-
-
path_info = Utils.unescape_path request.path_info
-
return fail(400, "Bad Request") unless Utils.valid_path?(path_info)
-
-
clean_path_info = Utils.clean_path_info(path_info)
-
path = ::File.join(@root, clean_path_info)
-
-
available = begin
-
::File.file?(path) && ::File.readable?(path)
-
rescue SystemCallError
-
false
-
end
-
-
if available
-
serving(request, path)
-
else
-
fail(404, "File not found: #{path_info}")
-
end
-
end
-
-
1
def serving(request, path)
-
if request.options?
-
return [200, {'Allow' => ALLOW_HEADER, CONTENT_LENGTH => '0'}, []]
-
end
-
last_modified = ::File.mtime(path).httpdate
-
return [304, {}, []] if request.get_header('HTTP_IF_MODIFIED_SINCE') == last_modified
-
-
headers = { "Last-Modified" => last_modified }
-
mime_type = mime_type path, @default_mime
-
headers[CONTENT_TYPE] = mime_type if mime_type
-
-
# Set custom headers
-
@headers.each { |field, content| headers[field] = content } if @headers
-
-
response = [ 200, headers ]
-
-
size = filesize path
-
-
range = nil
-
ranges = Rack::Utils.get_byte_ranges(request.get_header('HTTP_RANGE'), size)
-
if ranges.nil? || ranges.length > 1
-
# No ranges, or multiple ranges (which we don't support):
-
# TODO: Support multiple byte-ranges
-
response[0] = 200
-
range = 0..size-1
-
elsif ranges.empty?
-
# Unsatisfiable. Return error, and file size:
-
response = fail(416, "Byte range unsatisfiable")
-
response[1]["Content-Range"] = "bytes */#{size}"
-
return response
-
else
-
# Partial content:
-
range = ranges[0]
-
response[0] = 206
-
response[1]["Content-Range"] = "bytes #{range.begin}-#{range.end}/#{size}"
-
size = range.end - range.begin + 1
-
end
-
-
response[2] = [response_body] unless response_body.nil?
-
-
response[1][CONTENT_LENGTH] = size.to_s
-
response[2] = make_body request, path, range
-
response
-
end
-
-
1
class Iterator
-
1
attr_reader :path, :range
-
1
alias :to_path :path
-
-
1
def initialize path, range
-
@path = path
-
@range = range
-
end
-
-
1
def each
-
::File.open(path, "rb") do |file|
-
file.seek(range.begin)
-
remaining_len = range.end-range.begin+1
-
while remaining_len > 0
-
part = file.read([8192, remaining_len].min)
-
break unless part
-
remaining_len -= part.length
-
-
yield part
-
end
-
end
-
end
-
-
1
def close; end
-
end
-
-
1
private
-
-
1
def make_body request, path, range
-
if request.head?
-
[]
-
else
-
Iterator.new path, range
-
end
-
end
-
-
1
def fail(status, body, headers = {})
-
body += "\n"
-
-
[
-
status,
-
{
-
CONTENT_TYPE => "text/plain",
-
CONTENT_LENGTH => body.size.to_s,
-
"X-Cascade" => "pass"
-
}.merge!(headers),
-
[body]
-
]
-
end
-
-
# The MIME type for the contents of the file located at @path
-
1
def mime_type path, default_mime
-
Mime.mime_type(::File.extname(path), default_mime)
-
end
-
-
1
def filesize path
-
# If response_body is present, use its size.
-
return response_body.bytesize if response_body
-
-
# We check via File::size? whether this file provides size info
-
# via stat (e.g. /proc files often don't), otherwise we have to
-
# figure it out by reading the whole file into memory.
-
::File.size?(path) || ::File.read(path).bytesize
-
end
-
-
# By default, the response body for file requests is nil.
-
# In this case, the response body will be generated later
-
# from the file at @path
-
1
def response_body
-
nil
-
end
-
end
-
end
-
1
require 'rack/body_proxy'
-
-
1
module Rack
-
# Rack::Head returns an empty body for all HEAD requests. It leaves
-
# all other requests unchanged.
-
1
class Head
-
1
def initialize(app)
-
2
@app = app
-
end
-
-
1
def call(env)
-
2
status, headers, body = @app.call(env)
-
-
2
if env[REQUEST_METHOD] == HEAD
-
[
-
status, headers, Rack::BodyProxy.new([]) do
-
body.close if body.respond_to? :close
-
end
-
]
-
else
-
2
[status, headers, body]
-
end
-
end
-
end
-
end
-
1
require 'rack/utils'
-
1
require 'forwardable'
-
-
1
module Rack
-
# Rack::Lint validates your application and the requests and
-
# responses according to the Rack spec.
-
-
1
class Lint
-
1
def initialize(app)
-
@app = app
-
@content_length = nil
-
end
-
-
# :stopdoc:
-
-
1
class LintError < RuntimeError; end
-
1
module Assertion
-
1
def assert(message)
-
unless yield
-
raise LintError, message
-
end
-
end
-
end
-
1
include Assertion
-
-
## This specification aims to formalize the Rack protocol. You
-
## can (and should) use Rack::Lint to enforce it.
-
##
-
## When you develop middleware, be sure to add a Lint before and
-
## after to catch all mistakes.
-
-
## = Rack applications
-
-
## A Rack application is a Ruby object (not a class) that
-
## responds to +call+.
-
1
def call(env=nil)
-
dup._call(env)
-
end
-
-
1
def _call(env)
-
## It takes exactly one argument, the *environment*
-
assert("No env given") { env }
-
check_env env
-
-
env[RACK_INPUT] = InputWrapper.new(env[RACK_INPUT])
-
env[RACK_ERRORS] = ErrorWrapper.new(env[RACK_ERRORS])
-
-
## and returns an Array of exactly three values:
-
status, headers, @body = @app.call(env)
-
## The *status*,
-
check_status status
-
## the *headers*,
-
check_headers headers
-
-
check_hijack_response headers, env
-
-
## and the *body*.
-
check_content_type status, headers
-
check_content_length status, headers
-
@head_request = env[REQUEST_METHOD] == HEAD
-
[status, headers, self]
-
end
-
-
## == The Environment
-
1
def check_env(env)
-
## The environment must be an instance of Hash that includes
-
## CGI-like headers. The application is free to modify the
-
## environment.
-
assert("env #{env.inspect} is not a Hash, but #{env.class}") {
-
env.kind_of? Hash
-
}
-
-
##
-
## The environment is required to include these variables
-
## (adopted from PEP333), except when they'd be empty, but see
-
## below.
-
-
## <tt>REQUEST_METHOD</tt>:: The HTTP request method, such as
-
## "GET" or "POST". This cannot ever
-
## be an empty string, and so is
-
## always required.
-
-
## <tt>SCRIPT_NAME</tt>:: The initial portion of the request
-
## URL's "path" that corresponds to the
-
## application object, so that the
-
## application knows its virtual
-
## "location". This may be an empty
-
## string, if the application corresponds
-
## to the "root" of the server.
-
-
## <tt>PATH_INFO</tt>:: The remainder of the request URL's
-
## "path", designating the virtual
-
## "location" of the request's target
-
## within the application. This may be an
-
## empty string, if the request URL targets
-
## the application root and does not have a
-
## trailing slash. This value may be
-
## percent-encoded when originating from
-
## a URL.
-
-
## <tt>QUERY_STRING</tt>:: The portion of the request URL that
-
## follows the <tt>?</tt>, if any. May be
-
## empty, but is always required!
-
-
## <tt>SERVER_NAME</tt>, <tt>SERVER_PORT</tt>::
-
## When combined with <tt>SCRIPT_NAME</tt> and
-
## <tt>PATH_INFO</tt>, these variables can be
-
## used to complete the URL. Note, however,
-
## that <tt>HTTP_HOST</tt>, if present,
-
## should be used in preference to
-
## <tt>SERVER_NAME</tt> for reconstructing
-
## the request URL.
-
## <tt>SERVER_NAME</tt> and <tt>SERVER_PORT</tt>
-
## can never be empty strings, and so
-
## are always required.
-
-
## <tt>HTTP_</tt> Variables:: Variables corresponding to the
-
## client-supplied HTTP request
-
## headers (i.e., variables whose
-
## names begin with <tt>HTTP_</tt>). The
-
## presence or absence of these
-
## variables should correspond with
-
## the presence or absence of the
-
## appropriate HTTP header in the
-
## request. See
-
## <a href="https://tools.ietf.org/html/rfc3875#section-4.1.18">
-
## RFC3875 section 4.1.18</a> for
-
## specific behavior.
-
-
## In addition to this, the Rack environment must include these
-
## Rack-specific variables:
-
-
## <tt>rack.version</tt>:: The Array representing this version of Rack
-
## See Rack::VERSION, that corresponds to
-
## the version of this SPEC.
-
-
## <tt>rack.url_scheme</tt>:: +http+ or +https+, depending on the
-
## request URL.
-
-
## <tt>rack.input</tt>:: See below, the input stream.
-
-
## <tt>rack.errors</tt>:: See below, the error stream.
-
-
## <tt>rack.multithread</tt>:: true if the application object may be
-
## simultaneously invoked by another thread
-
## in the same process, false otherwise.
-
-
## <tt>rack.multiprocess</tt>:: true if an equivalent application object
-
## may be simultaneously invoked by another
-
## process, false otherwise.
-
-
## <tt>rack.run_once</tt>:: true if the server expects
-
## (but does not guarantee!) that the
-
## application will only be invoked this one
-
## time during the life of its containing
-
## process. Normally, this will only be true
-
## for a server based on CGI
-
## (or something similar).
-
-
## <tt>rack.hijack?</tt>:: present and true if the server supports
-
## connection hijacking. See below, hijacking.
-
-
## <tt>rack.hijack</tt>:: an object responding to #call that must be
-
## called at least once before using
-
## rack.hijack_io.
-
## It is recommended #call return rack.hijack_io
-
## as well as setting it in env if necessary.
-
-
## <tt>rack.hijack_io</tt>:: if rack.hijack? is true, and rack.hijack
-
## has received #call, this will contain
-
## an object resembling an IO. See hijacking.
-
-
## Additional environment specifications have approved to
-
## standardized middleware APIs. None of these are required to
-
## be implemented by the server.
-
-
## <tt>rack.session</tt>:: A hash like interface for storing
-
## request session data.
-
## The store must implement:
-
if session = env[RACK_SESSION]
-
## store(key, value) (aliased as []=);
-
assert("session #{session.inspect} must respond to store and []=") {
-
session.respond_to?(:store) && session.respond_to?(:[]=)
-
}
-
-
## fetch(key, default = nil) (aliased as []);
-
assert("session #{session.inspect} must respond to fetch and []") {
-
session.respond_to?(:fetch) && session.respond_to?(:[])
-
}
-
-
## delete(key);
-
assert("session #{session.inspect} must respond to delete") {
-
session.respond_to?(:delete)
-
}
-
-
## clear;
-
assert("session #{session.inspect} must respond to clear") {
-
session.respond_to?(:clear)
-
}
-
end
-
-
## <tt>rack.logger</tt>:: A common object interface for logging messages.
-
## The object must implement:
-
if logger = env[RACK_LOGGER]
-
## info(message, &block)
-
assert("logger #{logger.inspect} must respond to info") {
-
logger.respond_to?(:info)
-
}
-
-
## debug(message, &block)
-
assert("logger #{logger.inspect} must respond to debug") {
-
logger.respond_to?(:debug)
-
}
-
-
## warn(message, &block)
-
assert("logger #{logger.inspect} must respond to warn") {
-
logger.respond_to?(:warn)
-
}
-
-
## error(message, &block)
-
assert("logger #{logger.inspect} must respond to error") {
-
logger.respond_to?(:error)
-
}
-
-
## fatal(message, &block)
-
assert("logger #{logger.inspect} must respond to fatal") {
-
logger.respond_to?(:fatal)
-
}
-
end
-
-
## <tt>rack.multipart.buffer_size</tt>:: An Integer hint to the multipart parser as to what chunk size to use for reads and writes.
-
if bufsize = env[RACK_MULTIPART_BUFFER_SIZE]
-
assert("rack.multipart.buffer_size must be an Integer > 0 if specified") {
-
bufsize.is_a?(Integer) && bufsize > 0
-
}
-
end
-
-
## <tt>rack.multipart.tempfile_factory</tt>:: An object responding to #call with two arguments, the filename and content_type given for the multipart form field, and returning an IO-like object that responds to #<< and optionally #rewind. This factory will be used to instantiate the tempfile for each multipart form file upload field, rather than the default class of Tempfile.
-
if tempfile_factory = env[RACK_MULTIPART_TEMPFILE_FACTORY]
-
assert("rack.multipart.tempfile_factory must respond to #call") { tempfile_factory.respond_to?(:call) }
-
env[RACK_MULTIPART_TEMPFILE_FACTORY] = lambda do |filename, content_type|
-
io = tempfile_factory.call(filename, content_type)
-
assert("rack.multipart.tempfile_factory return value must respond to #<<") { io.respond_to?(:<<) }
-
io
-
end
-
end
-
-
## The server or the application can store their own data in the
-
## environment, too. The keys must contain at least one dot,
-
## and should be prefixed uniquely. The prefix <tt>rack.</tt>
-
## is reserved for use with the Rack core distribution and other
-
## accepted specifications and must not be used otherwise.
-
##
-
-
%w[REQUEST_METHOD SERVER_NAME SERVER_PORT
-
QUERY_STRING
-
rack.version rack.input rack.errors
-
rack.multithread rack.multiprocess rack.run_once].each { |header|
-
assert("env missing required key #{header}") { env.include? header }
-
}
-
-
## The environment must not contain the keys
-
## <tt>HTTP_CONTENT_TYPE</tt> or <tt>HTTP_CONTENT_LENGTH</tt>
-
## (use the versions without <tt>HTTP_</tt>).
-
%w[HTTP_CONTENT_TYPE HTTP_CONTENT_LENGTH].each { |header|
-
assert("env contains #{header}, must use #{header[5,-1]}") {
-
not env.include? header
-
}
-
}
-
-
## The CGI keys (named without a period) must have String values.
-
env.each { |key, value|
-
next if key.include? "." # Skip extensions
-
assert("env variable #{key} has non-string value #{value.inspect}") {
-
value.kind_of? String
-
}
-
}
-
-
## There are the following restrictions:
-
-
## * <tt>rack.version</tt> must be an array of Integers.
-
assert("rack.version must be an Array, was #{env[RACK_VERSION].class}") {
-
env[RACK_VERSION].kind_of? Array
-
}
-
## * <tt>rack.url_scheme</tt> must either be +http+ or +https+.
-
assert("rack.url_scheme unknown: #{env[RACK_URL_SCHEME].inspect}") {
-
%w[http https].include?(env[RACK_URL_SCHEME])
-
}
-
-
## * There must be a valid input stream in <tt>rack.input</tt>.
-
check_input env[RACK_INPUT]
-
## * There must be a valid error stream in <tt>rack.errors</tt>.
-
check_error env[RACK_ERRORS]
-
## * There may be a valid hijack stream in <tt>rack.hijack_io</tt>
-
check_hijack env
-
-
## * The <tt>REQUEST_METHOD</tt> must be a valid token.
-
assert("REQUEST_METHOD unknown: #{env[REQUEST_METHOD]}") {
-
env[REQUEST_METHOD] =~ /\A[0-9A-Za-z!\#$%&'*+.^_`|~-]+\z/
-
}
-
-
## * The <tt>SCRIPT_NAME</tt>, if non-empty, must start with <tt>/</tt>
-
assert("SCRIPT_NAME must start with /") {
-
!env.include?(SCRIPT_NAME) ||
-
env[SCRIPT_NAME] == "" ||
-
env[SCRIPT_NAME] =~ /\A\//
-
}
-
## * The <tt>PATH_INFO</tt>, if non-empty, must start with <tt>/</tt>
-
assert("PATH_INFO must start with /") {
-
!env.include?(PATH_INFO) ||
-
env[PATH_INFO] == "" ||
-
env[PATH_INFO] =~ /\A\//
-
}
-
## * The <tt>CONTENT_LENGTH</tt>, if given, must consist of digits only.
-
assert("Invalid CONTENT_LENGTH: #{env["CONTENT_LENGTH"]}") {
-
!env.include?("CONTENT_LENGTH") || env["CONTENT_LENGTH"] =~ /\A\d+\z/
-
}
-
-
## * One of <tt>SCRIPT_NAME</tt> or <tt>PATH_INFO</tt> must be
-
## set. <tt>PATH_INFO</tt> should be <tt>/</tt> if
-
## <tt>SCRIPT_NAME</tt> is empty.
-
assert("One of SCRIPT_NAME or PATH_INFO must be set (make PATH_INFO '/' if SCRIPT_NAME is empty)") {
-
env[SCRIPT_NAME] || env[PATH_INFO]
-
}
-
## <tt>SCRIPT_NAME</tt> never should be <tt>/</tt>, but instead be empty.
-
assert("SCRIPT_NAME cannot be '/', make it '' and PATH_INFO '/'") {
-
env[SCRIPT_NAME] != "/"
-
}
-
end
-
-
## === The Input Stream
-
##
-
## The input stream is an IO-like object which contains the raw HTTP
-
## POST data.
-
1
def check_input(input)
-
## When applicable, its external encoding must be "ASCII-8BIT" and it
-
## must be opened in binary mode, for Ruby 1.9 compatibility.
-
assert("rack.input #{input} does not have ASCII-8BIT as its external encoding") {
-
input.external_encoding.name == "ASCII-8BIT"
-
} if input.respond_to?(:external_encoding)
-
assert("rack.input #{input} is not opened in binary mode") {
-
input.binmode?
-
} if input.respond_to?(:binmode?)
-
-
## The input stream must respond to +gets+, +each+, +read+ and +rewind+.
-
[:gets, :each, :read, :rewind].each { |method|
-
assert("rack.input #{input} does not respond to ##{method}") {
-
input.respond_to? method
-
}
-
}
-
end
-
-
1
class InputWrapper
-
1
include Assertion
-
-
1
def initialize(input)
-
@input = input
-
end
-
-
## * +gets+ must be called without arguments and return a string,
-
## or +nil+ on EOF.
-
1
def gets(*args)
-
assert("rack.input#gets called with arguments") { args.size == 0 }
-
v = @input.gets
-
assert("rack.input#gets didn't return a String") {
-
v.nil? or v.kind_of? String
-
}
-
v
-
end
-
-
## * +read+ behaves like IO#read.
-
## Its signature is <tt>read([length, [buffer]])</tt>.
-
##
-
## If given, +length+ must be a non-negative Integer (>= 0) or +nil+,
-
## and +buffer+ must be a String and may not be nil.
-
##
-
## If +length+ is given and not nil, then this method reads at most
-
## +length+ bytes from the input stream.
-
##
-
## If +length+ is not given or nil, then this method reads
-
## all data until EOF.
-
##
-
## When EOF is reached, this method returns nil if +length+ is given
-
## and not nil, or "" if +length+ is not given or is nil.
-
##
-
## If +buffer+ is given, then the read data will be placed
-
## into +buffer+ instead of a newly created String object.
-
1
def read(*args)
-
assert("rack.input#read called with too many arguments") {
-
args.size <= 2
-
}
-
if args.size >= 1
-
assert("rack.input#read called with non-integer and non-nil length") {
-
args.first.kind_of?(Integer) || args.first.nil?
-
}
-
assert("rack.input#read called with a negative length") {
-
args.first.nil? || args.first >= 0
-
}
-
end
-
if args.size >= 2
-
assert("rack.input#read called with non-String buffer") {
-
args[1].kind_of?(String)
-
}
-
end
-
-
v = @input.read(*args)
-
-
assert("rack.input#read didn't return nil or a String") {
-
v.nil? or v.kind_of? String
-
}
-
if args[0].nil?
-
assert("rack.input#read(nil) returned nil on EOF") {
-
!v.nil?
-
}
-
end
-
-
v
-
end
-
-
## * +each+ must be called without arguments and only yield Strings.
-
1
def each(*args)
-
assert("rack.input#each called with arguments") { args.size == 0 }
-
@input.each { |line|
-
assert("rack.input#each didn't yield a String") {
-
line.kind_of? String
-
}
-
yield line
-
}
-
end
-
-
## * +rewind+ must be called without arguments. It rewinds the input
-
## stream back to the beginning. It must not raise Errno::ESPIPE:
-
## that is, it may not be a pipe or a socket. Therefore, handler
-
## developers must buffer the input data into some rewindable object
-
## if the underlying input stream is not rewindable.
-
1
def rewind(*args)
-
assert("rack.input#rewind called with arguments") { args.size == 0 }
-
assert("rack.input#rewind raised Errno::ESPIPE") {
-
begin
-
@input.rewind
-
true
-
rescue Errno::ESPIPE
-
false
-
end
-
}
-
end
-
-
## * +close+ must never be called on the input stream.
-
1
def close(*args)
-
assert("rack.input#close must not be called") { false }
-
end
-
end
-
-
## === The Error Stream
-
1
def check_error(error)
-
## The error stream must respond to +puts+, +write+ and +flush+.
-
[:puts, :write, :flush].each { |method|
-
assert("rack.error #{error} does not respond to ##{method}") {
-
error.respond_to? method
-
}
-
}
-
end
-
-
1
class ErrorWrapper
-
1
include Assertion
-
-
1
def initialize(error)
-
@error = error
-
end
-
-
## * +puts+ must be called with a single argument that responds to +to_s+.
-
1
def puts(str)
-
@error.puts str
-
end
-
-
## * +write+ must be called with a single argument that is a String.
-
1
def write(str)
-
assert("rack.errors#write not called with a String") { str.kind_of? String }
-
@error.write str
-
end
-
-
## * +flush+ must be called without arguments and must be called
-
## in order to make the error appear for sure.
-
1
def flush
-
@error.flush
-
end
-
-
## * +close+ must never be called on the error stream.
-
1
def close(*args)
-
assert("rack.errors#close must not be called") { false }
-
end
-
end
-
-
1
class HijackWrapper
-
1
include Assertion
-
1
extend Forwardable
-
-
1
REQUIRED_METHODS = [
-
:read, :write, :read_nonblock, :write_nonblock, :flush, :close,
-
:close_read, :close_write, :closed?
-
]
-
-
1
def_delegators :@io, *REQUIRED_METHODS
-
-
1
def initialize(io)
-
@io = io
-
REQUIRED_METHODS.each do |meth|
-
assert("rack.hijack_io must respond to #{meth}") { io.respond_to? meth }
-
end
-
end
-
end
-
-
## === Hijacking
-
#
-
# AUTHORS: n.b. The trailing whitespace between paragraphs is important and
-
# should not be removed. The whitespace creates paragraphs in the RDoc
-
# output.
-
#
-
## ==== Request (before status)
-
1
def check_hijack(env)
-
if env[RACK_IS_HIJACK]
-
## If rack.hijack? is true then rack.hijack must respond to #call.
-
original_hijack = env[RACK_HIJACK]
-
assert("rack.hijack must respond to call") { original_hijack.respond_to?(:call) }
-
env[RACK_HIJACK] = proc do
-
## rack.hijack must return the io that will also be assigned (or is
-
## already present, in rack.hijack_io.
-
io = original_hijack.call
-
HijackWrapper.new(io)
-
##
-
## rack.hijack_io must respond to:
-
## <tt>read, write, read_nonblock, write_nonblock, flush, close,
-
## close_read, close_write, closed?</tt>
-
##
-
## The semantics of these IO methods must be a best effort match to
-
## those of a normal ruby IO or Socket object, using standard
-
## arguments and raising standard exceptions. Servers are encouraged
-
## to simply pass on real IO objects, although it is recognized that
-
## this approach is not directly compatible with SPDY and HTTP 2.0.
-
##
-
## IO provided in rack.hijack_io should preference the
-
## IO::WaitReadable and IO::WaitWritable APIs wherever supported.
-
##
-
## There is a deliberate lack of full specification around
-
## rack.hijack_io, as semantics will change from server to server.
-
## Users are encouraged to utilize this API with a knowledge of their
-
## server choice, and servers may extend the functionality of
-
## hijack_io to provide additional features to users. The purpose of
-
## rack.hijack is for Rack to "get out of the way", as such, Rack only
-
## provides the minimum of specification and support.
-
env[RACK_HIJACK_IO] = HijackWrapper.new(env[RACK_HIJACK_IO])
-
io
-
end
-
else
-
##
-
## If rack.hijack? is false, then rack.hijack should not be set.
-
assert("rack.hijack? is false, but rack.hijack is present") { env[RACK_HIJACK].nil? }
-
##
-
## If rack.hijack? is false, then rack.hijack_io should not be set.
-
assert("rack.hijack? is false, but rack.hijack_io is present") { env[RACK_HIJACK_IO].nil? }
-
end
-
end
-
-
## ==== Response (after headers)
-
## It is also possible to hijack a response after the status and headers
-
## have been sent.
-
1
def check_hijack_response(headers, env)
-
-
# this check uses headers like a hash, but the spec only requires
-
# headers respond to #each
-
headers = Rack::Utils::HeaderHash.new(headers)
-
-
## In order to do this, an application may set the special header
-
## <tt>rack.hijack</tt> to an object that responds to <tt>call</tt>
-
## accepting an argument that conforms to the <tt>rack.hijack_io</tt>
-
## protocol.
-
##
-
## After the headers have been sent, and this hijack callback has been
-
## called, the application is now responsible for the remaining lifecycle
-
## of the IO. The application is also responsible for maintaining HTTP
-
## semantics. Of specific note, in almost all cases in the current SPEC,
-
## applications will have wanted to specify the header Connection:close in
-
## HTTP/1.1, and not Connection:keep-alive, as there is no protocol for
-
## returning hijacked sockets to the web server. For that purpose, use the
-
## body streaming API instead (progressively yielding strings via each).
-
##
-
## Servers must ignore the <tt>body</tt> part of the response tuple when
-
## the <tt>rack.hijack</tt> response API is in use.
-
-
if env[RACK_IS_HIJACK] && headers[RACK_HIJACK]
-
assert('rack.hijack header must respond to #call') {
-
headers[RACK_HIJACK].respond_to? :call
-
}
-
original_hijack = headers[RACK_HIJACK]
-
headers[RACK_HIJACK] = proc do |io|
-
original_hijack.call HijackWrapper.new(io)
-
end
-
else
-
##
-
## The special response header <tt>rack.hijack</tt> must only be set
-
## if the request env has <tt>rack.hijack?</tt> <tt>true</tt>.
-
assert('rack.hijack header must not be present if server does not support hijacking') {
-
headers[RACK_HIJACK].nil?
-
}
-
end
-
end
-
## ==== Conventions
-
## * Middleware should not use hijack unless it is handling the whole
-
## response.
-
## * Middleware may wrap the IO object for the response pattern.
-
## * Middleware should not wrap the IO object for the request pattern. The
-
## request pattern is intended to provide the hijacker with "raw tcp".
-
-
## == The Response
-
-
## === The Status
-
1
def check_status(status)
-
## This is an HTTP status. When parsed as integer (+to_i+), it must be
-
## greater than or equal to 100.
-
assert("Status must be >=100 seen as integer") { status.to_i >= 100 }
-
end
-
-
## === The Headers
-
1
def check_headers(header)
-
## The header must respond to +each+, and yield values of key and value.
-
assert("headers object should respond to #each, but doesn't (got #{header.class} as headers)") {
-
header.respond_to? :each
-
}
-
header.each { |key, value|
-
## Special headers starting "rack." are for communicating with the
-
## server, and must not be sent back to the client.
-
next if key =~ /^rack\..+$/
-
-
## The header keys must be Strings.
-
assert("header key must be a string, was #{key.class}") {
-
key.kind_of? String
-
}
-
## The header must not contain a +Status+ key.
-
assert("header must not contain Status") { key.downcase != "status" }
-
## The header must conform to RFC7230 token specification, i.e. cannot
-
## contain non-printable ASCII, DQUOTE or "(),/:;<=>?@[\]{}".
-
assert("invalid header name: #{key}") { key !~ /[\(\),\/:;<=>\?@\[\\\]{}[:cntrl:]]/ }
-
-
## The values of the header must be Strings,
-
assert("a header value must be a String, but the value of " +
-
"'#{key}' is a #{value.class}") { value.kind_of? String }
-
## consisting of lines (for multiple header values, e.g. multiple
-
## <tt>Set-Cookie</tt> values) separated by "\\n".
-
value.split("\n").each { |item|
-
## The lines must not contain characters below 037.
-
assert("invalid header value #{key}: #{item.inspect}") {
-
item !~ /[\000-\037]/
-
}
-
}
-
}
-
end
-
-
## === The Content-Type
-
1
def check_content_type(status, headers)
-
headers.each { |key, value|
-
## There must not be a <tt>Content-Type</tt>, when the +Status+ is 1xx,
-
## 204 or 304.
-
if key.downcase == "content-type"
-
assert("Content-Type header found in #{status} response, not allowed") {
-
not Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include? status.to_i
-
}
-
return
-
end
-
}
-
end
-
-
## === The Content-Length
-
1
def check_content_length(status, headers)
-
headers.each { |key, value|
-
if key.downcase == 'content-length'
-
## There must not be a <tt>Content-Length</tt> header when the
-
## +Status+ is 1xx, 204 or 304.
-
assert("Content-Length header found in #{status} response, not allowed") {
-
not Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include? status.to_i
-
}
-
@content_length = value
-
end
-
}
-
end
-
-
1
def verify_content_length(bytes)
-
if @head_request
-
assert("Response body was given for HEAD request, but should be empty") {
-
bytes == 0
-
}
-
elsif @content_length
-
assert("Content-Length header was #{@content_length}, but should be #{bytes}") {
-
@content_length == bytes.to_s
-
}
-
end
-
end
-
-
## === The Body
-
1
def each
-
@closed = false
-
bytes = 0
-
-
## The Body must respond to +each+
-
assert("Response body must respond to each") do
-
@body.respond_to?(:each)
-
end
-
-
@body.each { |part|
-
## and must only yield String values.
-
assert("Body yielded non-string value #{part.inspect}") {
-
part.kind_of? String
-
}
-
bytes += part.bytesize
-
yield part
-
}
-
verify_content_length(bytes)
-
-
##
-
## The Body itself should not be an instance of String, as this will
-
## break in Ruby 1.9.
-
##
-
## If the Body responds to +close+, it will be called after iteration. If
-
## the body is replaced by a middleware after action, the original body
-
## must be closed first, if it responds to close.
-
# XXX howto: assert("Body has not been closed") { @closed }
-
-
-
##
-
## If the Body responds to +to_path+, it must return a String
-
## identifying the location of a file whose contents are identical
-
## to that produced by calling +each+; this may be used by the
-
## server as an alternative, possibly more efficient way to
-
## transport the response.
-
-
if @body.respond_to?(:to_path)
-
assert("The file identified by body.to_path does not exist") {
-
::File.exist? @body.to_path
-
}
-
end
-
-
##
-
## The Body commonly is an Array of Strings, the application
-
## instance itself, or a File-like object.
-
end
-
-
1
def close
-
@closed = true
-
@body.close if @body.respond_to?(:close)
-
end
-
-
# :startdoc:
-
-
end
-
end
-
-
## == Thanks
-
## Some parts of this specification are adopted from PEP333: Python
-
## Web Server Gateway Interface
-
## v1.0 (http://www.python.org/dev/peps/pep-0333/). I'd like to thank
-
## everyone involved in that effort.
-
1
module Rack
-
1
class MethodOverride
-
1
HTTP_METHODS = %w[GET HEAD PUT POST DELETE OPTIONS PATCH LINK UNLINK]
-
-
1
METHOD_OVERRIDE_PARAM_KEY = "_method".freeze
-
1
HTTP_METHOD_OVERRIDE_HEADER = "HTTP_X_HTTP_METHOD_OVERRIDE".freeze
-
1
ALLOWED_METHODS = %w[POST]
-
-
1
def initialize(app)
-
1
@app = app
-
end
-
-
1
def call(env)
-
2
if allowed_methods.include?(env[REQUEST_METHOD])
-
method = method_override(env)
-
if HTTP_METHODS.include?(method)
-
env[RACK_METHODOVERRIDE_ORIGINAL_METHOD] = env[REQUEST_METHOD]
-
env[REQUEST_METHOD] = method
-
end
-
end
-
-
2
@app.call(env)
-
end
-
-
1
def method_override(env)
-
req = Request.new(env)
-
method = method_override_param(req) ||
-
env[HTTP_METHOD_OVERRIDE_HEADER]
-
begin
-
method.to_s.upcase
-
rescue ArgumentError
-
env[RACK_ERRORS].puts "Invalid string for method"
-
end
-
end
-
-
1
private
-
-
1
def allowed_methods
-
2
ALLOWED_METHODS
-
end
-
-
1
def method_override_param(req)
-
req.POST[METHOD_OVERRIDE_PARAM_KEY]
-
rescue Utils::InvalidParameterError, Utils::ParameterTypeError
-
req.get_header(RACK_ERRORS).puts "Invalid or incomplete POST params"
-
rescue EOFError
-
req.get_header(RACK_ERRORS).puts "Bad request content body"
-
end
-
end
-
end
-
1
module Rack
-
1
module Mime
-
# Returns String with mime type if found, otherwise use +fallback+.
-
# +ext+ should be filename extension in the '.ext' format that
-
# File.extname(file) returns.
-
# +fallback+ may be any object
-
#
-
# Also see the documentation for MIME_TYPES
-
#
-
# Usage:
-
# Rack::Mime.mime_type('.foo')
-
#
-
# This is a shortcut for:
-
# Rack::Mime::MIME_TYPES.fetch('.foo', 'application/octet-stream')
-
-
1
def mime_type(ext, fallback='application/octet-stream')
-
MIME_TYPES.fetch(ext.to_s.downcase, fallback)
-
end
-
1
module_function :mime_type
-
-
# Returns true if the given value is a mime match for the given mime match
-
# specification, false otherwise.
-
#
-
# Rack::Mime.match?('text/html', 'text/*') => true
-
# Rack::Mime.match?('text/plain', '*') => true
-
# Rack::Mime.match?('text/html', 'application/json') => false
-
-
1
def match?(value, matcher)
-
v1, v2 = value.split('/', 2)
-
m1, m2 = matcher.split('/', 2)
-
-
(m1 == '*' || v1 == m1) && (m2.nil? || m2 == '*' || m2 == v2)
-
end
-
1
module_function :match?
-
-
# List of most common mime-types, selected various sources
-
# according to their usefulness in a webserving scope for Ruby
-
# users.
-
#
-
# To amend this list with your local mime.types list you can use:
-
#
-
# require 'webrick/httputils'
-
# list = WEBrick::HTTPUtils.load_mime_types('/etc/mime.types')
-
# Rack::Mime::MIME_TYPES.merge!(list)
-
#
-
# N.B. On Ubuntu the mime.types file does not include the leading period, so
-
# users may need to modify the data before merging into the hash.
-
-
1
MIME_TYPES = {
-
".123" => "application/vnd.lotus-1-2-3",
-
".3dml" => "text/vnd.in3d.3dml",
-
".3g2" => "video/3gpp2",
-
".3gp" => "video/3gpp",
-
".a" => "application/octet-stream",
-
".acc" => "application/vnd.americandynamics.acc",
-
".ace" => "application/x-ace-compressed",
-
".acu" => "application/vnd.acucobol",
-
".aep" => "application/vnd.audiograph",
-
".afp" => "application/vnd.ibm.modcap",
-
".ai" => "application/postscript",
-
".aif" => "audio/x-aiff",
-
".aiff" => "audio/x-aiff",
-
".ami" => "application/vnd.amiga.ami",
-
".appcache" => "text/cache-manifest",
-
".apr" => "application/vnd.lotus-approach",
-
".asc" => "application/pgp-signature",
-
".asf" => "video/x-ms-asf",
-
".asm" => "text/x-asm",
-
".aso" => "application/vnd.accpac.simply.aso",
-
".asx" => "video/x-ms-asf",
-
".atc" => "application/vnd.acucorp",
-
".atom" => "application/atom+xml",
-
".atomcat" => "application/atomcat+xml",
-
".atomsvc" => "application/atomsvc+xml",
-
".atx" => "application/vnd.antix.game-component",
-
".au" => "audio/basic",
-
".avi" => "video/x-msvideo",
-
".bat" => "application/x-msdownload",
-
".bcpio" => "application/x-bcpio",
-
".bdm" => "application/vnd.syncml.dm+wbxml",
-
".bh2" => "application/vnd.fujitsu.oasysprs",
-
".bin" => "application/octet-stream",
-
".bmi" => "application/vnd.bmi",
-
".bmp" => "image/bmp",
-
".box" => "application/vnd.previewsystems.box",
-
".btif" => "image/prs.btif",
-
".bz" => "application/x-bzip",
-
".bz2" => "application/x-bzip2",
-
".c" => "text/x-c",
-
".c4g" => "application/vnd.clonk.c4group",
-
".cab" => "application/vnd.ms-cab-compressed",
-
".cc" => "text/x-c",
-
".ccxml" => "application/ccxml+xml",
-
".cdbcmsg" => "application/vnd.contact.cmsg",
-
".cdkey" => "application/vnd.mediastation.cdkey",
-
".cdx" => "chemical/x-cdx",
-
".cdxml" => "application/vnd.chemdraw+xml",
-
".cdy" => "application/vnd.cinderella",
-
".cer" => "application/pkix-cert",
-
".cgm" => "image/cgm",
-
".chat" => "application/x-chat",
-
".chm" => "application/vnd.ms-htmlhelp",
-
".chrt" => "application/vnd.kde.kchart",
-
".cif" => "chemical/x-cif",
-
".cii" => "application/vnd.anser-web-certificate-issue-initiation",
-
".cil" => "application/vnd.ms-artgalry",
-
".cla" => "application/vnd.claymore",
-
".class" => "application/octet-stream",
-
".clkk" => "application/vnd.crick.clicker.keyboard",
-
".clkp" => "application/vnd.crick.clicker.palette",
-
".clkt" => "application/vnd.crick.clicker.template",
-
".clkw" => "application/vnd.crick.clicker.wordbank",
-
".clkx" => "application/vnd.crick.clicker",
-
".clp" => "application/x-msclip",
-
".cmc" => "application/vnd.cosmocaller",
-
".cmdf" => "chemical/x-cmdf",
-
".cml" => "chemical/x-cml",
-
".cmp" => "application/vnd.yellowriver-custom-menu",
-
".cmx" => "image/x-cmx",
-
".com" => "application/x-msdownload",
-
".conf" => "text/plain",
-
".cpio" => "application/x-cpio",
-
".cpp" => "text/x-c",
-
".cpt" => "application/mac-compactpro",
-
".crd" => "application/x-mscardfile",
-
".crl" => "application/pkix-crl",
-
".crt" => "application/x-x509-ca-cert",
-
".csh" => "application/x-csh",
-
".csml" => "chemical/x-csml",
-
".csp" => "application/vnd.commonspace",
-
".css" => "text/css",
-
".csv" => "text/csv",
-
".curl" => "application/vnd.curl",
-
".cww" => "application/prs.cww",
-
".cxx" => "text/x-c",
-
".daf" => "application/vnd.mobius.daf",
-
".davmount" => "application/davmount+xml",
-
".dcr" => "application/x-director",
-
".dd2" => "application/vnd.oma.dd2+xml",
-
".ddd" => "application/vnd.fujixerox.ddd",
-
".deb" => "application/x-debian-package",
-
".der" => "application/x-x509-ca-cert",
-
".dfac" => "application/vnd.dreamfactory",
-
".diff" => "text/x-diff",
-
".dis" => "application/vnd.mobius.dis",
-
".djv" => "image/vnd.djvu",
-
".djvu" => "image/vnd.djvu",
-
".dll" => "application/x-msdownload",
-
".dmg" => "application/octet-stream",
-
".dna" => "application/vnd.dna",
-
".doc" => "application/msword",
-
".docm" => "application/vnd.ms-word.document.macroEnabled.12",
-
".docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
-
".dot" => "application/msword",
-
".dotm" => "application/vnd.ms-word.template.macroEnabled.12",
-
".dotx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.template",
-
".dp" => "application/vnd.osgi.dp",
-
".dpg" => "application/vnd.dpgraph",
-
".dsc" => "text/prs.lines.tag",
-
".dtd" => "application/xml-dtd",
-
".dts" => "audio/vnd.dts",
-
".dtshd" => "audio/vnd.dts.hd",
-
".dv" => "video/x-dv",
-
".dvi" => "application/x-dvi",
-
".dwf" => "model/vnd.dwf",
-
".dwg" => "image/vnd.dwg",
-
".dxf" => "image/vnd.dxf",
-
".dxp" => "application/vnd.spotfire.dxp",
-
".ear" => "application/java-archive",
-
".ecelp4800" => "audio/vnd.nuera.ecelp4800",
-
".ecelp7470" => "audio/vnd.nuera.ecelp7470",
-
".ecelp9600" => "audio/vnd.nuera.ecelp9600",
-
".ecma" => "application/ecmascript",
-
".edm" => "application/vnd.novadigm.edm",
-
".edx" => "application/vnd.novadigm.edx",
-
".efif" => "application/vnd.picsel",
-
".ei6" => "application/vnd.pg.osasli",
-
".eml" => "message/rfc822",
-
".eol" => "audio/vnd.digital-winds",
-
".eot" => "application/vnd.ms-fontobject",
-
".eps" => "application/postscript",
-
".es3" => "application/vnd.eszigno3+xml",
-
".esf" => "application/vnd.epson.esf",
-
".etx" => "text/x-setext",
-
".exe" => "application/x-msdownload",
-
".ext" => "application/vnd.novadigm.ext",
-
".ez" => "application/andrew-inset",
-
".ez2" => "application/vnd.ezpix-album",
-
".ez3" => "application/vnd.ezpix-package",
-
".f" => "text/x-fortran",
-
".f77" => "text/x-fortran",
-
".f90" => "text/x-fortran",
-
".fbs" => "image/vnd.fastbidsheet",
-
".fdf" => "application/vnd.fdf",
-
".fe_launch" => "application/vnd.denovo.fcselayout-link",
-
".fg5" => "application/vnd.fujitsu.oasysgp",
-
".fli" => "video/x-fli",
-
".flo" => "application/vnd.micrografx.flo",
-
".flv" => "video/x-flv",
-
".flw" => "application/vnd.kde.kivio",
-
".flx" => "text/vnd.fmi.flexstor",
-
".fly" => "text/vnd.fly",
-
".fm" => "application/vnd.framemaker",
-
".fnc" => "application/vnd.frogans.fnc",
-
".for" => "text/x-fortran",
-
".fpx" => "image/vnd.fpx",
-
".fsc" => "application/vnd.fsc.weblaunch",
-
".fst" => "image/vnd.fst",
-
".ftc" => "application/vnd.fluxtime.clip",
-
".fti" => "application/vnd.anser-web-funds-transfer-initiation",
-
".fvt" => "video/vnd.fvt",
-
".fzs" => "application/vnd.fuzzysheet",
-
".g3" => "image/g3fax",
-
".gac" => "application/vnd.groove-account",
-
".gdl" => "model/vnd.gdl",
-
".gem" => "application/octet-stream",
-
".gemspec" => "text/x-script.ruby",
-
".ghf" => "application/vnd.groove-help",
-
".gif" => "image/gif",
-
".gim" => "application/vnd.groove-identity-message",
-
".gmx" => "application/vnd.gmx",
-
".gph" => "application/vnd.flographit",
-
".gqf" => "application/vnd.grafeq",
-
".gram" => "application/srgs",
-
".grv" => "application/vnd.groove-injector",
-
".grxml" => "application/srgs+xml",
-
".gtar" => "application/x-gtar",
-
".gtm" => "application/vnd.groove-tool-message",
-
".gtw" => "model/vnd.gtw",
-
".gv" => "text/vnd.graphviz",
-
".gz" => "application/x-gzip",
-
".h" => "text/x-c",
-
".h261" => "video/h261",
-
".h263" => "video/h263",
-
".h264" => "video/h264",
-
".hbci" => "application/vnd.hbci",
-
".hdf" => "application/x-hdf",
-
".hh" => "text/x-c",
-
".hlp" => "application/winhlp",
-
".hpgl" => "application/vnd.hp-hpgl",
-
".hpid" => "application/vnd.hp-hpid",
-
".hps" => "application/vnd.hp-hps",
-
".hqx" => "application/mac-binhex40",
-
".htc" => "text/x-component",
-
".htke" => "application/vnd.kenameaapp",
-
".htm" => "text/html",
-
".html" => "text/html",
-
".hvd" => "application/vnd.yamaha.hv-dic",
-
".hvp" => "application/vnd.yamaha.hv-voice",
-
".hvs" => "application/vnd.yamaha.hv-script",
-
".icc" => "application/vnd.iccprofile",
-
".ice" => "x-conference/x-cooltalk",
-
".ico" => "image/vnd.microsoft.icon",
-
".ics" => "text/calendar",
-
".ief" => "image/ief",
-
".ifb" => "text/calendar",
-
".ifm" => "application/vnd.shana.informed.formdata",
-
".igl" => "application/vnd.igloader",
-
".igs" => "model/iges",
-
".igx" => "application/vnd.micrografx.igx",
-
".iif" => "application/vnd.shana.informed.interchange",
-
".imp" => "application/vnd.accpac.simply.imp",
-
".ims" => "application/vnd.ms-ims",
-
".ipk" => "application/vnd.shana.informed.package",
-
".irm" => "application/vnd.ibm.rights-management",
-
".irp" => "application/vnd.irepository.package+xml",
-
".iso" => "application/octet-stream",
-
".itp" => "application/vnd.shana.informed.formtemplate",
-
".ivp" => "application/vnd.immervision-ivp",
-
".ivu" => "application/vnd.immervision-ivu",
-
".jad" => "text/vnd.sun.j2me.app-descriptor",
-
".jam" => "application/vnd.jam",
-
".jar" => "application/java-archive",
-
".java" => "text/x-java-source",
-
".jisp" => "application/vnd.jisp",
-
".jlt" => "application/vnd.hp-jlyt",
-
".jnlp" => "application/x-java-jnlp-file",
-
".joda" => "application/vnd.joost.joda-archive",
-
".jp2" => "image/jp2",
-
".jpeg" => "image/jpeg",
-
".jpg" => "image/jpeg",
-
".jpgv" => "video/jpeg",
-
".jpm" => "video/jpm",
-
".js" => "application/javascript",
-
".json" => "application/json",
-
".karbon" => "application/vnd.kde.karbon",
-
".kfo" => "application/vnd.kde.kformula",
-
".kia" => "application/vnd.kidspiration",
-
".kml" => "application/vnd.google-earth.kml+xml",
-
".kmz" => "application/vnd.google-earth.kmz",
-
".kne" => "application/vnd.kinar",
-
".kon" => "application/vnd.kde.kontour",
-
".kpr" => "application/vnd.kde.kpresenter",
-
".ksp" => "application/vnd.kde.kspread",
-
".ktz" => "application/vnd.kahootz",
-
".kwd" => "application/vnd.kde.kword",
-
".latex" => "application/x-latex",
-
".lbd" => "application/vnd.llamagraphics.life-balance.desktop",
-
".lbe" => "application/vnd.llamagraphics.life-balance.exchange+xml",
-
".les" => "application/vnd.hhe.lesson-player",
-
".link66" => "application/vnd.route66.link66+xml",
-
".log" => "text/plain",
-
".lostxml" => "application/lost+xml",
-
".lrm" => "application/vnd.ms-lrm",
-
".ltf" => "application/vnd.frogans.ltf",
-
".lvp" => "audio/vnd.lucent.voice",
-
".lwp" => "application/vnd.lotus-wordpro",
-
".m3u" => "audio/x-mpegurl",
-
".m4a" => "audio/mp4a-latm",
-
".m4v" => "video/mp4",
-
".ma" => "application/mathematica",
-
".mag" => "application/vnd.ecowin.chart",
-
".man" => "text/troff",
-
".manifest" => "text/cache-manifest",
-
".mathml" => "application/mathml+xml",
-
".mbk" => "application/vnd.mobius.mbk",
-
".mbox" => "application/mbox",
-
".mc1" => "application/vnd.medcalcdata",
-
".mcd" => "application/vnd.mcd",
-
".mdb" => "application/x-msaccess",
-
".mdi" => "image/vnd.ms-modi",
-
".mdoc" => "text/troff",
-
".me" => "text/troff",
-
".mfm" => "application/vnd.mfmp",
-
".mgz" => "application/vnd.proteus.magazine",
-
".mid" => "audio/midi",
-
".midi" => "audio/midi",
-
".mif" => "application/vnd.mif",
-
".mime" => "message/rfc822",
-
".mj2" => "video/mj2",
-
".mlp" => "application/vnd.dolby.mlp",
-
".mmd" => "application/vnd.chipnuts.karaoke-mmd",
-
".mmf" => "application/vnd.smaf",
-
".mml" => "application/mathml+xml",
-
".mmr" => "image/vnd.fujixerox.edmics-mmr",
-
".mng" => "video/x-mng",
-
".mny" => "application/x-msmoney",
-
".mov" => "video/quicktime",
-
".movie" => "video/x-sgi-movie",
-
".mp3" => "audio/mpeg",
-
".mp4" => "video/mp4",
-
".mp4a" => "audio/mp4",
-
".mp4s" => "application/mp4",
-
".mp4v" => "video/mp4",
-
".mpc" => "application/vnd.mophun.certificate",
-
".mpeg" => "video/mpeg",
-
".mpg" => "video/mpeg",
-
".mpga" => "audio/mpeg",
-
".mpkg" => "application/vnd.apple.installer+xml",
-
".mpm" => "application/vnd.blueice.multipass",
-
".mpn" => "application/vnd.mophun.application",
-
".mpp" => "application/vnd.ms-project",
-
".mpy" => "application/vnd.ibm.minipay",
-
".mqy" => "application/vnd.mobius.mqy",
-
".mrc" => "application/marc",
-
".ms" => "text/troff",
-
".mscml" => "application/mediaservercontrol+xml",
-
".mseq" => "application/vnd.mseq",
-
".msf" => "application/vnd.epson.msf",
-
".msh" => "model/mesh",
-
".msi" => "application/x-msdownload",
-
".msl" => "application/vnd.mobius.msl",
-
".msty" => "application/vnd.muvee.style",
-
".mts" => "model/vnd.mts",
-
".mus" => "application/vnd.musician",
-
".mvb" => "application/x-msmediaview",
-
".mwf" => "application/vnd.mfer",
-
".mxf" => "application/mxf",
-
".mxl" => "application/vnd.recordare.musicxml",
-
".mxml" => "application/xv+xml",
-
".mxs" => "application/vnd.triscape.mxs",
-
".mxu" => "video/vnd.mpegurl",
-
".n" => "application/vnd.nokia.n-gage.symbian.install",
-
".nc" => "application/x-netcdf",
-
".ngdat" => "application/vnd.nokia.n-gage.data",
-
".nlu" => "application/vnd.neurolanguage.nlu",
-
".nml" => "application/vnd.enliven",
-
".nnd" => "application/vnd.noblenet-directory",
-
".nns" => "application/vnd.noblenet-sealer",
-
".nnw" => "application/vnd.noblenet-web",
-
".npx" => "image/vnd.net-fpx",
-
".nsf" => "application/vnd.lotus-notes",
-
".oa2" => "application/vnd.fujitsu.oasys2",
-
".oa3" => "application/vnd.fujitsu.oasys3",
-
".oas" => "application/vnd.fujitsu.oasys",
-
".obd" => "application/x-msbinder",
-
".oda" => "application/oda",
-
".odc" => "application/vnd.oasis.opendocument.chart",
-
".odf" => "application/vnd.oasis.opendocument.formula",
-
".odg" => "application/vnd.oasis.opendocument.graphics",
-
".odi" => "application/vnd.oasis.opendocument.image",
-
".odp" => "application/vnd.oasis.opendocument.presentation",
-
".ods" => "application/vnd.oasis.opendocument.spreadsheet",
-
".odt" => "application/vnd.oasis.opendocument.text",
-
".oga" => "audio/ogg",
-
".ogg" => "application/ogg",
-
".ogv" => "video/ogg",
-
".ogx" => "application/ogg",
-
".org" => "application/vnd.lotus-organizer",
-
".otc" => "application/vnd.oasis.opendocument.chart-template",
-
".otf" => "application/vnd.oasis.opendocument.formula-template",
-
".otg" => "application/vnd.oasis.opendocument.graphics-template",
-
".oth" => "application/vnd.oasis.opendocument.text-web",
-
".oti" => "application/vnd.oasis.opendocument.image-template",
-
".otm" => "application/vnd.oasis.opendocument.text-master",
-
".ots" => "application/vnd.oasis.opendocument.spreadsheet-template",
-
".ott" => "application/vnd.oasis.opendocument.text-template",
-
".oxt" => "application/vnd.openofficeorg.extension",
-
".p" => "text/x-pascal",
-
".p10" => "application/pkcs10",
-
".p12" => "application/x-pkcs12",
-
".p7b" => "application/x-pkcs7-certificates",
-
".p7m" => "application/pkcs7-mime",
-
".p7r" => "application/x-pkcs7-certreqresp",
-
".p7s" => "application/pkcs7-signature",
-
".pas" => "text/x-pascal",
-
".pbd" => "application/vnd.powerbuilder6",
-
".pbm" => "image/x-portable-bitmap",
-
".pcl" => "application/vnd.hp-pcl",
-
".pclxl" => "application/vnd.hp-pclxl",
-
".pcx" => "image/x-pcx",
-
".pdb" => "chemical/x-pdb",
-
".pdf" => "application/pdf",
-
".pem" => "application/x-x509-ca-cert",
-
".pfr" => "application/font-tdpfr",
-
".pgm" => "image/x-portable-graymap",
-
".pgn" => "application/x-chess-pgn",
-
".pgp" => "application/pgp-encrypted",
-
".pic" => "image/x-pict",
-
".pict" => "image/pict",
-
".pkg" => "application/octet-stream",
-
".pki" => "application/pkixcmp",
-
".pkipath" => "application/pkix-pkipath",
-
".pl" => "text/x-script.perl",
-
".plb" => "application/vnd.3gpp.pic-bw-large",
-
".plc" => "application/vnd.mobius.plc",
-
".plf" => "application/vnd.pocketlearn",
-
".pls" => "application/pls+xml",
-
".pm" => "text/x-script.perl-module",
-
".pml" => "application/vnd.ctc-posml",
-
".png" => "image/png",
-
".pnm" => "image/x-portable-anymap",
-
".pntg" => "image/x-macpaint",
-
".portpkg" => "application/vnd.macports.portpkg",
-
".pot" => "application/vnd.ms-powerpoint",
-
".potm" => "application/vnd.ms-powerpoint.template.macroEnabled.12",
-
".potx" => "application/vnd.openxmlformats-officedocument.presentationml.template",
-
".ppa" => "application/vnd.ms-powerpoint",
-
".ppam" => "application/vnd.ms-powerpoint.addin.macroEnabled.12",
-
".ppd" => "application/vnd.cups-ppd",
-
".ppm" => "image/x-portable-pixmap",
-
".pps" => "application/vnd.ms-powerpoint",
-
".ppsm" => "application/vnd.ms-powerpoint.slideshow.macroEnabled.12",
-
".ppsx" => "application/vnd.openxmlformats-officedocument.presentationml.slideshow",
-
".ppt" => "application/vnd.ms-powerpoint",
-
".pptm" => "application/vnd.ms-powerpoint.presentation.macroEnabled.12",
-
".pptx" => "application/vnd.openxmlformats-officedocument.presentationml.presentation",
-
".prc" => "application/vnd.palm",
-
".pre" => "application/vnd.lotus-freelance",
-
".prf" => "application/pics-rules",
-
".ps" => "application/postscript",
-
".psb" => "application/vnd.3gpp.pic-bw-small",
-
".psd" => "image/vnd.adobe.photoshop",
-
".ptid" => "application/vnd.pvi.ptid1",
-
".pub" => "application/x-mspublisher",
-
".pvb" => "application/vnd.3gpp.pic-bw-var",
-
".pwn" => "application/vnd.3m.post-it-notes",
-
".py" => "text/x-script.python",
-
".pya" => "audio/vnd.ms-playready.media.pya",
-
".pyv" => "video/vnd.ms-playready.media.pyv",
-
".qam" => "application/vnd.epson.quickanime",
-
".qbo" => "application/vnd.intu.qbo",
-
".qfx" => "application/vnd.intu.qfx",
-
".qps" => "application/vnd.publishare-delta-tree",
-
".qt" => "video/quicktime",
-
".qtif" => "image/x-quicktime",
-
".qxd" => "application/vnd.quark.quarkxpress",
-
".ra" => "audio/x-pn-realaudio",
-
".rake" => "text/x-script.ruby",
-
".ram" => "audio/x-pn-realaudio",
-
".rar" => "application/x-rar-compressed",
-
".ras" => "image/x-cmu-raster",
-
".rb" => "text/x-script.ruby",
-
".rcprofile" => "application/vnd.ipunplugged.rcprofile",
-
".rdf" => "application/rdf+xml",
-
".rdz" => "application/vnd.data-vision.rdz",
-
".rep" => "application/vnd.businessobjects",
-
".rgb" => "image/x-rgb",
-
".rif" => "application/reginfo+xml",
-
".rl" => "application/resource-lists+xml",
-
".rlc" => "image/vnd.fujixerox.edmics-rlc",
-
".rld" => "application/resource-lists-diff+xml",
-
".rm" => "application/vnd.rn-realmedia",
-
".rmp" => "audio/x-pn-realaudio-plugin",
-
".rms" => "application/vnd.jcp.javame.midlet-rms",
-
".rnc" => "application/relax-ng-compact-syntax",
-
".roff" => "text/troff",
-
".rpm" => "application/x-redhat-package-manager",
-
".rpss" => "application/vnd.nokia.radio-presets",
-
".rpst" => "application/vnd.nokia.radio-preset",
-
".rq" => "application/sparql-query",
-
".rs" => "application/rls-services+xml",
-
".rsd" => "application/rsd+xml",
-
".rss" => "application/rss+xml",
-
".rtf" => "application/rtf",
-
".rtx" => "text/richtext",
-
".ru" => "text/x-script.ruby",
-
".s" => "text/x-asm",
-
".saf" => "application/vnd.yamaha.smaf-audio",
-
".sbml" => "application/sbml+xml",
-
".sc" => "application/vnd.ibm.secure-container",
-
".scd" => "application/x-msschedule",
-
".scm" => "application/vnd.lotus-screencam",
-
".scq" => "application/scvp-cv-request",
-
".scs" => "application/scvp-cv-response",
-
".sdkm" => "application/vnd.solent.sdkm+xml",
-
".sdp" => "application/sdp",
-
".see" => "application/vnd.seemail",
-
".sema" => "application/vnd.sema",
-
".semd" => "application/vnd.semd",
-
".semf" => "application/vnd.semf",
-
".setpay" => "application/set-payment-initiation",
-
".setreg" => "application/set-registration-initiation",
-
".sfd" => "application/vnd.hydrostatix.sof-data",
-
".sfs" => "application/vnd.spotfire.sfs",
-
".sgm" => "text/sgml",
-
".sgml" => "text/sgml",
-
".sh" => "application/x-sh",
-
".shar" => "application/x-shar",
-
".shf" => "application/shf+xml",
-
".sig" => "application/pgp-signature",
-
".sit" => "application/x-stuffit",
-
".sitx" => "application/x-stuffitx",
-
".skp" => "application/vnd.koan",
-
".slt" => "application/vnd.epson.salt",
-
".smi" => "application/smil+xml",
-
".snd" => "audio/basic",
-
".so" => "application/octet-stream",
-
".spf" => "application/vnd.yamaha.smaf-phrase",
-
".spl" => "application/x-futuresplash",
-
".spot" => "text/vnd.in3d.spot",
-
".spp" => "application/scvp-vp-response",
-
".spq" => "application/scvp-vp-request",
-
".src" => "application/x-wais-source",
-
".srx" => "application/sparql-results+xml",
-
".sse" => "application/vnd.kodak-descriptor",
-
".ssf" => "application/vnd.epson.ssf",
-
".ssml" => "application/ssml+xml",
-
".stf" => "application/vnd.wt.stf",
-
".stk" => "application/hyperstudio",
-
".str" => "application/vnd.pg.format",
-
".sus" => "application/vnd.sus-calendar",
-
".sv4cpio" => "application/x-sv4cpio",
-
".sv4crc" => "application/x-sv4crc",
-
".svd" => "application/vnd.svd",
-
".svg" => "image/svg+xml",
-
".svgz" => "image/svg+xml",
-
".swf" => "application/x-shockwave-flash",
-
".swi" => "application/vnd.arastra.swi",
-
".t" => "text/troff",
-
".tao" => "application/vnd.tao.intent-module-archive",
-
".tar" => "application/x-tar",
-
".tbz" => "application/x-bzip-compressed-tar",
-
".tcap" => "application/vnd.3gpp2.tcap",
-
".tcl" => "application/x-tcl",
-
".tex" => "application/x-tex",
-
".texi" => "application/x-texinfo",
-
".texinfo" => "application/x-texinfo",
-
".text" => "text/plain",
-
".tif" => "image/tiff",
-
".tiff" => "image/tiff",
-
".tmo" => "application/vnd.tmobile-livetv",
-
".torrent" => "application/x-bittorrent",
-
".tpl" => "application/vnd.groove-tool-template",
-
".tpt" => "application/vnd.trid.tpt",
-
".tr" => "text/troff",
-
".tra" => "application/vnd.trueapp",
-
".trm" => "application/x-msterminal",
-
".tsv" => "text/tab-separated-values",
-
".ttf" => "application/octet-stream",
-
".twd" => "application/vnd.simtech-mindmapper",
-
".txd" => "application/vnd.genomatix.tuxedo",
-
".txf" => "application/vnd.mobius.txf",
-
".txt" => "text/plain",
-
".ufd" => "application/vnd.ufdl",
-
".umj" => "application/vnd.umajin",
-
".unityweb" => "application/vnd.unity",
-
".uoml" => "application/vnd.uoml+xml",
-
".uri" => "text/uri-list",
-
".ustar" => "application/x-ustar",
-
".utz" => "application/vnd.uiq.theme",
-
".uu" => "text/x-uuencode",
-
".vcd" => "application/x-cdlink",
-
".vcf" => "text/x-vcard",
-
".vcg" => "application/vnd.groove-vcard",
-
".vcs" => "text/x-vcalendar",
-
".vcx" => "application/vnd.vcx",
-
".vis" => "application/vnd.visionary",
-
".viv" => "video/vnd.vivo",
-
".vrml" => "model/vrml",
-
".vsd" => "application/vnd.visio",
-
".vsf" => "application/vnd.vsf",
-
".vtu" => "model/vnd.vtu",
-
".vxml" => "application/voicexml+xml",
-
".war" => "application/java-archive",
-
".wav" => "audio/x-wav",
-
".wax" => "audio/x-ms-wax",
-
".wbmp" => "image/vnd.wap.wbmp",
-
".wbs" => "application/vnd.criticaltools.wbs+xml",
-
".wbxml" => "application/vnd.wap.wbxml",
-
".webm" => "video/webm",
-
".wm" => "video/x-ms-wm",
-
".wma" => "audio/x-ms-wma",
-
".wmd" => "application/x-ms-wmd",
-
".wmf" => "application/x-msmetafile",
-
".wml" => "text/vnd.wap.wml",
-
".wmlc" => "application/vnd.wap.wmlc",
-
".wmls" => "text/vnd.wap.wmlscript",
-
".wmlsc" => "application/vnd.wap.wmlscriptc",
-
".wmv" => "video/x-ms-wmv",
-
".wmx" => "video/x-ms-wmx",
-
".wmz" => "application/x-ms-wmz",
-
".woff" => "application/font-woff",
-
".woff2" => "application/font-woff2",
-
".wpd" => "application/vnd.wordperfect",
-
".wpl" => "application/vnd.ms-wpl",
-
".wps" => "application/vnd.ms-works",
-
".wqd" => "application/vnd.wqd",
-
".wri" => "application/x-mswrite",
-
".wrl" => "model/vrml",
-
".wsdl" => "application/wsdl+xml",
-
".wspolicy" => "application/wspolicy+xml",
-
".wtb" => "application/vnd.webturbo",
-
".wvx" => "video/x-ms-wvx",
-
".x3d" => "application/vnd.hzn-3d-crossword",
-
".xar" => "application/vnd.xara",
-
".xbd" => "application/vnd.fujixerox.docuworks.binder",
-
".xbm" => "image/x-xbitmap",
-
".xdm" => "application/vnd.syncml.dm+xml",
-
".xdp" => "application/vnd.adobe.xdp+xml",
-
".xdw" => "application/vnd.fujixerox.docuworks",
-
".xenc" => "application/xenc+xml",
-
".xer" => "application/patch-ops-error+xml",
-
".xfdf" => "application/vnd.adobe.xfdf",
-
".xfdl" => "application/vnd.xfdl",
-
".xhtml" => "application/xhtml+xml",
-
".xif" => "image/vnd.xiff",
-
".xla" => "application/vnd.ms-excel",
-
".xlam" => "application/vnd.ms-excel.addin.macroEnabled.12",
-
".xls" => "application/vnd.ms-excel",
-
".xlsb" => "application/vnd.ms-excel.sheet.binary.macroEnabled.12",
-
".xlsx" => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
-
".xlsm" => "application/vnd.ms-excel.sheet.macroEnabled.12",
-
".xlt" => "application/vnd.ms-excel",
-
".xltx" => "application/vnd.openxmlformats-officedocument.spreadsheetml.template",
-
".xml" => "application/xml",
-
".xo" => "application/vnd.olpc-sugar",
-
".xop" => "application/xop+xml",
-
".xpm" => "image/x-xpixmap",
-
".xpr" => "application/vnd.is-xpr",
-
".xps" => "application/vnd.ms-xpsdocument",
-
".xpw" => "application/vnd.intercon.formnet",
-
".xsl" => "application/xml",
-
".xslt" => "application/xslt+xml",
-
".xsm" => "application/vnd.syncml+xml",
-
".xspf" => "application/xspf+xml",
-
".xul" => "application/vnd.mozilla.xul+xml",
-
".xwd" => "image/x-xwindowdump",
-
".xyz" => "chemical/x-xyz",
-
".yaml" => "text/yaml",
-
".yml" => "text/yaml",
-
".zaz" => "application/vnd.zzazz.deck+xml",
-
".zip" => "application/zip",
-
".zmm" => "application/vnd.handheld-entertainment+xml",
-
}
-
end
-
end
-
1
require 'uri'
-
1
require 'stringio'
-
1
require 'rack'
-
1
require 'rack/lint'
-
1
require 'rack/utils'
-
1
require 'rack/response'
-
-
1
module Rack
-
# Rack::MockRequest helps testing your Rack application without
-
# actually using HTTP.
-
#
-
# After performing a request on a URL with get/post/put/patch/delete, it
-
# returns a MockResponse with useful helper methods for effective
-
# testing.
-
#
-
# You can pass a hash with additional configuration to the
-
# get/post/put/patch/delete.
-
# <tt>:input</tt>:: A String or IO-like to be used as rack.input.
-
# <tt>:fatal</tt>:: Raise a FatalWarning if the app writes to rack.errors.
-
# <tt>:lint</tt>:: If true, wrap the application in a Rack::Lint.
-
-
1
class MockRequest
-
1
class FatalWarning < RuntimeError
-
end
-
-
1
class FatalWarner
-
1
def puts(warning)
-
raise FatalWarning, warning
-
end
-
-
1
def write(warning)
-
raise FatalWarning, warning
-
end
-
-
1
def flush
-
end
-
-
1
def string
-
""
-
end
-
end
-
-
DEFAULT_ENV = {
-
1
RACK_VERSION => Rack::VERSION,
-
RACK_INPUT => StringIO.new,
-
RACK_ERRORS => StringIO.new,
-
RACK_MULTITHREAD => true,
-
RACK_MULTIPROCESS => true,
-
RACK_RUNONCE => false,
-
}.freeze
-
-
1
def initialize(app)
-
@app = app
-
end
-
-
1
def get(uri, opts={}) request(GET, uri, opts) end
-
1
def post(uri, opts={}) request(POST, uri, opts) end
-
1
def put(uri, opts={}) request(PUT, uri, opts) end
-
1
def patch(uri, opts={}) request(PATCH, uri, opts) end
-
1
def delete(uri, opts={}) request(DELETE, uri, opts) end
-
1
def head(uri, opts={}) request(HEAD, uri, opts) end
-
1
def options(uri, opts={}) request(OPTIONS, uri, opts) end
-
-
1
def request(method=GET, uri="", opts={})
-
env = self.class.env_for(uri, opts.merge(:method => method))
-
-
if opts[:lint]
-
app = Rack::Lint.new(@app)
-
else
-
app = @app
-
end
-
-
errors = env[RACK_ERRORS]
-
status, headers, body = app.call(env)
-
MockResponse.new(status, headers, body, errors)
-
ensure
-
body.close if body.respond_to?(:close)
-
end
-
-
# For historical reasons, we're pinning to RFC 2396.
-
# URI::Parser = URI::RFC2396_Parser
-
1
def self.parse_uri_rfc2396(uri)
-
3
@parser ||= URI::Parser.new
-
3
@parser.parse(uri)
-
end
-
-
# Return the Rack environment used for a request to +uri+.
-
1
def self.env_for(uri="", opts={})
-
3
uri = parse_uri_rfc2396(uri)
-
3
uri.path = "/#{uri.path}" unless uri.path[0] == ?/
-
-
3
env = DEFAULT_ENV.dup
-
-
3
env[REQUEST_METHOD] = (opts[:method] ? opts[:method].to_s.upcase : GET).b
-
3
env[SERVER_NAME] = (uri.host || "example.org").b
-
3
env[SERVER_PORT] = (uri.port ? uri.port.to_s : "80").b
-
3
env[QUERY_STRING] = (uri.query.to_s).b
-
3
env[PATH_INFO] = ((!uri.path || uri.path.empty?) ? "/" : uri.path).b
-
3
env[RACK_URL_SCHEME] = (uri.scheme || "http").b
-
3
env[HTTPS] = (env[RACK_URL_SCHEME] == "https" ? "on" : "off").b
-
-
3
env[SCRIPT_NAME] = opts[:script_name] || ""
-
-
3
if opts[:fatal]
-
env[RACK_ERRORS] = FatalWarner.new
-
else
-
3
env[RACK_ERRORS] = StringIO.new
-
end
-
-
3
if params = opts[:params]
-
if env[REQUEST_METHOD] == GET
-
params = Utils.parse_nested_query(params) if params.is_a?(String)
-
params.update(Utils.parse_nested_query(env[QUERY_STRING]))
-
env[QUERY_STRING] = Utils.build_nested_query(params)
-
elsif !opts.has_key?(:input)
-
opts["CONTENT_TYPE"] = "application/x-www-form-urlencoded"
-
if params.is_a?(Hash)
-
if data = Rack::Multipart.build_multipart(params)
-
opts[:input] = data
-
opts["CONTENT_LENGTH"] ||= data.length.to_s
-
opts["CONTENT_TYPE"] = "multipart/form-data; boundary=#{Rack::Multipart::MULTIPART_BOUNDARY}"
-
else
-
opts[:input] = Utils.build_nested_query(params)
-
end
-
else
-
opts[:input] = params
-
end
-
end
-
end
-
-
3
empty_str = String.new
-
3
opts[:input] ||= empty_str
-
3
if String === opts[:input]
-
3
rack_input = StringIO.new(opts[:input])
-
else
-
rack_input = opts[:input]
-
end
-
-
3
rack_input.set_encoding(Encoding::BINARY)
-
3
env[RACK_INPUT] = rack_input
-
-
3
env["CONTENT_LENGTH"] ||= env[RACK_INPUT].length.to_s
-
-
3
opts.each { |field, value|
-
30
env[field] = value if String === field
-
}
-
-
3
env
-
end
-
end
-
-
# Rack::MockResponse provides useful helpers for testing your apps.
-
# Usually, you don't create the MockResponse on your own, but use
-
# MockRequest.
-
-
1
class MockResponse < Rack::Response
-
# Headers
-
1
attr_reader :original_headers
-
-
# Errors
-
1
attr_accessor :errors
-
-
1
def initialize(status, headers, body, errors=StringIO.new(""))
-
2
@original_headers = headers
-
2
@errors = errors.string if errors.respond_to?(:string)
-
-
2
super(body, status, headers)
-
end
-
-
1
def =~(other)
-
body =~ other
-
end
-
-
1
def match(other)
-
body.match other
-
end
-
-
1
def body
-
# FIXME: apparently users of MockResponse expect the return value of
-
# MockResponse#body to be a string. However, the real response object
-
# returns the body as a list.
-
#
-
# See spec_showstatus.rb:
-
#
-
# should "not replace existing messages" do
-
# ...
-
# res.body.should == "foo!"
-
# end
-
2
super.join
-
end
-
-
1
def empty?
-
[201, 204, 304].include? status
-
end
-
end
-
end
-
1
require 'rack/utils'
-
-
1
module Rack
-
# Sets an "X-Runtime" response header, indicating the response
-
# time of the request, in seconds
-
#
-
# You can put it right before the application to see the processing
-
# time, or before all the other middlewares to include time for them,
-
# too.
-
1
class Runtime
-
1
FORMAT_STRING = "%0.6f".freeze # :nodoc:
-
1
HEADER_NAME = "X-Runtime".freeze # :nodoc:
-
-
1
def initialize(app, name = nil)
-
1
@app = app
-
1
@header_name = HEADER_NAME
-
1
@header_name += "-#{name}" if name
-
end
-
-
1
def call(env)
-
2
start_time = Utils.clock_time
-
2
status, headers, body = @app.call(env)
-
2
request_time = Utils.clock_time - start_time
-
-
2
unless headers.has_key?(@header_name)
-
2
headers[@header_name] = FORMAT_STRING % request_time
-
end
-
-
2
[status, headers, body]
-
end
-
end
-
end
-
1
require 'rack/file'
-
1
require 'rack/body_proxy'
-
-
1
module Rack
-
-
# = Sendfile
-
#
-
# The Sendfile middleware intercepts responses whose body is being
-
# served from a file and replaces it with a server specific X-Sendfile
-
# header. The web server is then responsible for writing the file contents
-
# to the client. This can dramatically reduce the amount of work required
-
# by the Ruby backend and takes advantage of the web server's optimized file
-
# delivery code.
-
#
-
# In order to take advantage of this middleware, the response body must
-
# respond to +to_path+ and the request must include an X-Sendfile-Type
-
# header. Rack::File and other components implement +to_path+ so there's
-
# rarely anything you need to do in your application. The X-Sendfile-Type
-
# header is typically set in your web servers configuration. The following
-
# sections attempt to document
-
#
-
# === Nginx
-
#
-
# Nginx supports the X-Accel-Redirect header. This is similar to X-Sendfile
-
# but requires parts of the filesystem to be mapped into a private URL
-
# hierarchy.
-
#
-
# The following example shows the Nginx configuration required to create
-
# a private "/files/" area, enable X-Accel-Redirect, and pass the special
-
# X-Sendfile-Type and X-Accel-Mapping headers to the backend:
-
#
-
# location ~ /files/(.*) {
-
# internal;
-
# alias /var/www/$1;
-
# }
-
#
-
# location / {
-
# proxy_redirect off;
-
#
-
# proxy_set_header Host $host;
-
# proxy_set_header X-Real-IP $remote_addr;
-
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
-
#
-
# proxy_set_header X-Sendfile-Type X-Accel-Redirect;
-
# proxy_set_header X-Accel-Mapping /var/www/=/files/;
-
#
-
# proxy_pass http://127.0.0.1:8080/;
-
# }
-
#
-
# Note that the X-Sendfile-Type header must be set exactly as shown above.
-
# The X-Accel-Mapping header should specify the location on the file system,
-
# followed by an equals sign (=), followed name of the private URL pattern
-
# that it maps to. The middleware performs a simple substitution on the
-
# resulting path.
-
#
-
# See Also: http://wiki.codemongers.com/NginxXSendfile
-
#
-
# === lighttpd
-
#
-
# Lighttpd has supported some variation of the X-Sendfile header for some
-
# time, although only recent version support X-Sendfile in a reverse proxy
-
# configuration.
-
#
-
# $HTTP["host"] == "example.com" {
-
# proxy-core.protocol = "http"
-
# proxy-core.balancer = "round-robin"
-
# proxy-core.backends = (
-
# "127.0.0.1:8000",
-
# "127.0.0.1:8001",
-
# ...
-
# )
-
#
-
# proxy-core.allow-x-sendfile = "enable"
-
# proxy-core.rewrite-request = (
-
# "X-Sendfile-Type" => (".*" => "X-Sendfile")
-
# )
-
# }
-
#
-
# See Also: http://redmine.lighttpd.net/wiki/lighttpd/Docs:ModProxyCore
-
#
-
# === Apache
-
#
-
# X-Sendfile is supported under Apache 2.x using a separate module:
-
#
-
# https://tn123.org/mod_xsendfile/
-
#
-
# Once the module is compiled and installed, you can enable it using
-
# XSendFile config directive:
-
#
-
# RequestHeader Set X-Sendfile-Type X-Sendfile
-
# ProxyPassReverse / http://localhost:8001/
-
# XSendFile on
-
#
-
# === Mapping parameter
-
#
-
# The third parameter allows for an overriding extension of the
-
# X-Accel-Mapping header. Mappings should be provided in tuples of internal to
-
# external. The internal values may contain regular expression syntax, they
-
# will be matched with case indifference.
-
-
1
class Sendfile
-
1
def initialize(app, variation=nil, mappings=[])
-
1
@app = app
-
1
@variation = variation
-
1
@mappings = mappings.map do |internal, external|
-
[/^#{internal}/i, external]
-
end
-
end
-
-
1
def call(env)
-
2
status, headers, body = @app.call(env)
-
2
if body.respond_to?(:to_path)
-
case type = variation(env)
-
when 'X-Accel-Redirect'
-
path = ::File.expand_path(body.to_path)
-
if url = map_accel_path(env, path)
-
headers[CONTENT_LENGTH] = '0'
-
headers[type] = url
-
obody = body
-
body = Rack::BodyProxy.new([]) do
-
obody.close if obody.respond_to?(:close)
-
end
-
else
-
env[RACK_ERRORS].puts "X-Accel-Mapping header missing"
-
end
-
when 'X-Sendfile', 'X-Lighttpd-Send-File'
-
path = ::File.expand_path(body.to_path)
-
headers[CONTENT_LENGTH] = '0'
-
headers[type] = path
-
obody = body
-
body = Rack::BodyProxy.new([]) do
-
obody.close if obody.respond_to?(:close)
-
end
-
when '', nil
-
else
-
env[RACK_ERRORS].puts "Unknown x-sendfile variation: '#{type}'.\n"
-
end
-
end
-
2
[status, headers, body]
-
end
-
-
1
private
-
1
def variation(env)
-
@variation ||
-
env['sendfile.type'] ||
-
env['HTTP_X_SENDFILE_TYPE']
-
end
-
-
1
def map_accel_path(env, path)
-
if mapping = @mappings.find { |internal,_| internal =~ path }
-
path.sub(*mapping)
-
elsif mapping = env['HTTP_X_ACCEL_MAPPING']
-
internal, external = mapping.split('=', 2).map(&:strip)
-
path.sub(/^#{internal}/i, external)
-
end
-
end
-
end
-
end
-
1
require 'openssl'
-
1
require 'zlib'
-
1
require 'rack/request'
-
1
require 'rack/response'
-
1
require 'rack/session/abstract/id'
-
1
require 'json'
-
-
1
module Rack
-
-
1
module Session
-
-
# Rack::Session::Cookie provides simple cookie based session management.
-
# By default, the session is a Ruby Hash stored as base64 encoded marshalled
-
# data set to :key (default: rack.session). The object that encodes the
-
# session data is configurable and must respond to +encode+ and +decode+.
-
# Both methods must take a string and return a string.
-
#
-
# When the secret key is set, cookie data is checked for data integrity.
-
# The old secret key is also accepted and allows graceful secret rotation.
-
#
-
# Example:
-
#
-
# use Rack::Session::Cookie, :key => 'rack.session',
-
# :domain => 'foo.com',
-
# :path => '/',
-
# :expire_after => 2592000,
-
# :secret => 'change_me',
-
# :old_secret => 'also_change_me'
-
#
-
# All parameters are optional.
-
#
-
# Example of a cookie with no encoding:
-
#
-
# Rack::Session::Cookie.new(application, {
-
# :coder => Rack::Session::Cookie::Identity.new
-
# })
-
#
-
# Example of a cookie with custom encoding:
-
#
-
# Rack::Session::Cookie.new(application, {
-
# :coder => Class.new {
-
# def encode(str); str.reverse; end
-
# def decode(str); str.reverse; end
-
# }.new
-
# })
-
#
-
-
1
class Cookie < Abstract::Persisted
-
# Encode session cookies as Base64
-
1
class Base64
-
1
def encode(str)
-
[str].pack('m')
-
end
-
-
1
def decode(str)
-
str.unpack('m').first
-
end
-
-
# Encode session cookies as Marshaled Base64 data
-
1
class Marshal < Base64
-
1
def encode(str)
-
super(::Marshal.dump(str))
-
end
-
-
1
def decode(str)
-
return unless str
-
::Marshal.load(super(str)) rescue nil
-
end
-
end
-
-
# N.B. Unlike other encoding methods, the contained objects must be a
-
# valid JSON composite type, either a Hash or an Array.
-
1
class JSON < Base64
-
1
def encode(obj)
-
super(::JSON.dump(obj))
-
end
-
-
1
def decode(str)
-
return unless str
-
::JSON.parse(super(str)) rescue nil
-
end
-
end
-
-
1
class ZipJSON < Base64
-
1
def encode(obj)
-
super(Zlib::Deflate.deflate(::JSON.dump(obj)))
-
end
-
-
1
def decode(str)
-
return unless str
-
::JSON.parse(Zlib::Inflate.inflate(super(str)))
-
rescue
-
nil
-
end
-
end
-
end
-
-
# Use no encoding for session cookies
-
1
class Identity
-
1
def encode(str); str; end
-
1
def decode(str); str; end
-
end
-
-
1
attr_reader :coder
-
-
1
def initialize(app, options={})
-
@secrets = options.values_at(:secret, :old_secret).compact
-
@hmac = options.fetch(:hmac, OpenSSL::Digest::SHA1)
-
-
warn <<-MSG unless secure?(options)
-
SECURITY WARNING: No secret option provided to Rack::Session::Cookie.
-
This poses a security threat. It is strongly recommended that you
-
provide a secret to prevent exploits that may be possible from crafted
-
cookies. This will not be supported in future versions of Rack, and
-
future versions will even invalidate your existing user cookies.
-
-
Called from: #{caller[0]}.
-
MSG
-
@coder = options[:coder] ||= Base64::Marshal.new
-
super(app, options.merge!(:cookie_only => true))
-
end
-
-
1
private
-
-
1
def find_session(req, sid)
-
data = unpacked_cookie_data(req)
-
data = persistent_session_id!(data)
-
[data["session_id"], data]
-
end
-
-
1
def extract_session_id(request)
-
unpacked_cookie_data(request)["session_id"]
-
end
-
-
1
def unpacked_cookie_data(request)
-
request.fetch_header(RACK_SESSION_UNPACKED_COOKIE_DATA) do |k|
-
session_data = request.cookies[@key]
-
-
if @secrets.size > 0 && session_data
-
digest, session_data = session_data.reverse.split("--", 2)
-
digest.reverse! if digest
-
session_data.reverse! if session_data
-
session_data = nil unless digest_match?(session_data, digest)
-
end
-
-
request.set_header(k, coder.decode(session_data) || {})
-
end
-
end
-
-
1
def persistent_session_id!(data, sid=nil)
-
data ||= {}
-
data["session_id"] ||= sid || generate_sid
-
data
-
end
-
-
1
def write_session(req, session_id, session, options)
-
session = session.merge("session_id" => session_id)
-
session_data = coder.encode(session)
-
-
if @secrets.first
-
session_data << "--#{generate_hmac(session_data, @secrets.first)}"
-
end
-
-
if session_data.size > (4096 - @key.size)
-
req.get_header(RACK_ERRORS).puts("Warning! Rack::Session::Cookie data size exceeds 4K.")
-
nil
-
else
-
session_data
-
end
-
end
-
-
1
def delete_session(req, session_id, options)
-
# Nothing to do here, data is in the client
-
generate_sid unless options[:drop]
-
end
-
-
1
def digest_match?(data, digest)
-
return unless data && digest
-
@secrets.any? do |secret|
-
Rack::Utils.secure_compare(digest, generate_hmac(data, secret))
-
end
-
end
-
-
1
def generate_hmac(data, secret)
-
OpenSSL::HMAC.hexdigest(@hmac.new, secret, data)
-
end
-
-
1
def secure?(options)
-
@secrets.size >= 1 ||
-
(options[:coder] && options[:let_coder_handle_secure_encoding])
-
end
-
-
end
-
end
-
end
-
1
require 'rack/body_proxy'
-
-
1
module Rack
-
-
# Middleware tracks and cleans Tempfiles created throughout a request (i.e. Rack::Multipart)
-
# Ideas/strategy based on posts by Eric Wong and Charles Oliver Nutter
-
# https://groups.google.com/forum/#!searchin/rack-devel/temp/rack-devel/brK8eh-MByw/sw61oJJCGRMJ
-
1
class TempfileReaper
-
1
def initialize(app)
-
1
@app = app
-
end
-
-
1
def call(env)
-
2
env[RACK_TEMPFILES] ||= []
-
2
status, headers, body = @app.call(env)
-
2
body_proxy = BodyProxy.new(body) do
-
2
env[RACK_TEMPFILES].each(&:close!) unless env[RACK_TEMPFILES].nil?
-
end
-
2
[status, headers, body_proxy]
-
end
-
end
-
end
-
1
require 'rails/dom/testing/assertions'
-
1
require 'active_support/concern'
-
1
require 'nokogiri'
-
-
1
module Rails
-
1
module Dom
-
1
module Testing
-
1
module Assertions
-
1
autoload :DomAssertions, 'rails/dom/testing/assertions/dom_assertions'
-
1
autoload :SelectorAssertions, 'rails/dom/testing/assertions/selector_assertions'
-
-
1
extend ActiveSupport::Concern
-
-
1
include DomAssertions
-
1
include SelectorAssertions
-
end
-
end
-
end
-
end
-
1
module Rails
-
1
module Dom
-
1
module Testing
-
1
module Assertions
-
1
module DomAssertions
-
# \Test two HTML strings for equivalency (e.g., equal even when attributes are in another order)
-
#
-
# # assert that the referenced method generates the appropriate HTML string
-
# assert_dom_equal '<a href="http://www.example.com">Apples</a>', link_to("Apples", "http://www.example.com")
-
1
def assert_dom_equal(expected, actual, message = nil)
-
expected_dom, actual_dom = fragment(expected), fragment(actual)
-
message ||= "Expected: #{expected}\nActual: #{actual}"
-
assert compare_doms(expected_dom, actual_dom), message
-
end
-
-
# The negated form of +assert_dom_equal+.
-
#
-
# # assert that the referenced method does not generate the specified HTML string
-
# assert_dom_not_equal '<a href="http://www.example.com">Apples</a>', link_to("Oranges", "http://www.example.com")
-
1
def assert_dom_not_equal(expected, actual, message = nil)
-
expected_dom, actual_dom = fragment(expected), fragment(actual)
-
message ||= "Expected: #{expected}\nActual: #{actual}"
-
assert_not compare_doms(expected_dom, actual_dom), message
-
end
-
-
1
protected
-
-
1
def compare_doms(expected, actual)
-
return false unless expected.children.size == actual.children.size
-
-
expected.children.each_with_index do |child, i|
-
return false unless equal_children?(child, actual.children[i])
-
end
-
-
true
-
end
-
-
1
def equal_children?(child, other_child)
-
return false unless child.type == other_child.type
-
-
if child.element?
-
child.name == other_child.name &&
-
equal_attribute_nodes?(child.attribute_nodes, other_child.attribute_nodes) &&
-
compare_doms(child, other_child)
-
else
-
child.to_s == other_child.to_s
-
end
-
end
-
-
1
def equal_attribute_nodes?(nodes, other_nodes)
-
return false unless nodes.size == other_nodes.size
-
-
nodes = nodes.sort_by(&:name)
-
other_nodes = other_nodes.sort_by(&:name)
-
-
nodes.each_with_index do |attr, i|
-
return false unless equal_attribute?(attr, other_nodes[i])
-
end
-
-
true
-
end
-
-
1
def equal_attribute?(attr, other_attr)
-
attr.name == other_attr.name && attr.value == other_attr.value
-
end
-
-
1
private
-
-
1
def fragment(text)
-
Nokogiri::HTML::DocumentFragment.parse(text)
-
end
-
end
-
end
-
end
-
end
-
end
-
1
require 'active_support/deprecation'
-
1
require_relative 'selector_assertions/count_describable'
-
1
require_relative 'selector_assertions/html_selector'
-
-
1
module Rails
-
1
module Dom
-
1
module Testing
-
1
module Assertions
-
# Adds the +assert_select+ method for use in Rails functional
-
# test cases, which can be used to make assertions on the response HTML of a controller
-
# action. You can also call +assert_select+ within another +assert_select+ to
-
# make assertions on elements selected by the enclosing assertion.
-
#
-
# Use +css_select+ to select elements without making an assertions, either
-
# from the response HTML or elements selected by the enclosing assertion.
-
#
-
# In addition to HTML responses, you can make the following assertions:
-
#
-
# * +assert_select_encoded+ - Assertions on HTML encoded inside XML, for example for dealing with feed item descriptions.
-
# * +assert_select_email+ - Assertions on the HTML body of an e-mail.
-
1
module SelectorAssertions
-
-
# Select and return all matching elements.
-
#
-
# If called with a single argument, uses that argument as a selector.
-
# Called without an element +css_select+ selects from
-
# the element returned in +document_root_element+
-
#
-
# The default implementation of +document_root_element+ raises an exception explaining this.
-
#
-
# Returns an empty Nokogiri::XML::NodeSet if no match is found.
-
#
-
# If called with two arguments, uses the first argument as the root
-
# element and the second argument as the selector. Attempts to match the
-
# root element and any of its children.
-
# Returns an empty Nokogiri::XML::NodeSet if no match is found.
-
#
-
# The selector may be a CSS selector expression (String).
-
# css_select returns nil if called with an invalid css selector.
-
#
-
# # Selects all div tags
-
# divs = css_select("div")
-
#
-
# # Selects all paragraph tags and does something interesting
-
# pars = css_select("p")
-
# pars.each do |par|
-
# # Do something fun with paragraphs here...
-
# end
-
#
-
# # Selects all list items in unordered lists
-
# items = css_select("ul>li")
-
#
-
# # Selects all form tags and then all inputs inside the form
-
# forms = css_select("form")
-
# forms.each do |form|
-
# inputs = css_select(form, "input")
-
# ...
-
# end
-
1
def css_select(*args)
-
raise ArgumentError, "you at least need a selector argument" if args.empty?
-
-
root = args.size == 1 ? document_root_element : args.shift
-
-
nodeset(root).css(args.first)
-
end
-
-
# An assertion that selects elements and makes one or more equality tests.
-
#
-
# If the first argument is an element, selects all matching elements
-
# starting from (and including) that element and all its children in
-
# depth-first order.
-
#
-
# If no element is specified +assert_select+ selects from
-
# the element returned in +document_root_element+
-
# unless +assert_select+ is called from within an +assert_select+ block.
-
# Override +document_root_element+ to tell +assert_select+ what to select from.
-
# The default implementation raises an exception explaining this.
-
#
-
# When called with a block +assert_select+ passes an array of selected elements
-
# to the block. Calling +assert_select+ from the block, with no element specified,
-
# runs the assertion on the complete set of elements selected by the enclosing assertion.
-
# Alternatively the array may be iterated through so that +assert_select+ can be called
-
# separately for each element.
-
#
-
#
-
# ==== Example
-
# If the response contains two ordered lists, each with four list elements then:
-
# assert_select "ol" do |elements|
-
# elements.each do |element|
-
# assert_select element, "li", 4
-
# end
-
# end
-
#
-
# will pass, as will:
-
# assert_select "ol" do
-
# assert_select "li", 8
-
# end
-
#
-
# The selector may be a CSS selector expression (String) or an expression
-
# with substitution values (Array).
-
# Substitution uses a custom pseudo class match. Pass in whatever attribute you want to match (enclosed in quotes) and a ? for the substitution.
-
# assert_select returns nil if called with an invalid css selector.
-
#
-
# assert_select "div:match('id', ?)", /\d+/
-
#
-
# === Equality Tests
-
#
-
# The equality test may be one of the following:
-
# * <tt>true</tt> - Assertion is true if at least one element selected.
-
# * <tt>false</tt> - Assertion is true if no element selected.
-
# * <tt>String/Regexp</tt> - Assertion is true if the text value of at least
-
# one element matches the string or regular expression.
-
# * <tt>Integer</tt> - Assertion is true if exactly that number of
-
# elements are selected.
-
# * <tt>Range</tt> - Assertion is true if the number of selected
-
# elements fit the range.
-
# If no equality test specified, the assertion is true if at least one
-
# element selected.
-
#
-
# To perform more than one equality tests, use a hash with the following keys:
-
# * <tt>:text</tt> - Narrow the selection to elements that have this text
-
# value (string or regexp).
-
# * <tt>:html</tt> - Narrow the selection to elements that have this HTML
-
# content (string or regexp).
-
# * <tt>:count</tt> - Assertion is true if the number of selected elements
-
# is equal to this value.
-
# * <tt>:minimum</tt> - Assertion is true if the number of selected
-
# elements is at least this value.
-
# * <tt>:maximum</tt> - Assertion is true if the number of selected
-
# elements is at most this value.
-
#
-
# If the method is called with a block, once all equality tests are
-
# evaluated the block is called with an array of all matched elements.
-
#
-
# # At least one form element
-
# assert_select "form"
-
#
-
# # Form element includes four input fields
-
# assert_select "form input", 4
-
#
-
# # Page title is "Welcome"
-
# assert_select "title", "Welcome"
-
#
-
# # Page title is "Welcome" and there is only one title element
-
# assert_select "title", {count: 1, text: "Welcome"},
-
# "Wrong title or more than one title element"
-
#
-
# # Page contains no forms
-
# assert_select "form", false, "This page must contain no forms"
-
#
-
# # Test the content and style
-
# assert_select "body div.header ul.menu"
-
#
-
# # Use substitution values
-
# assert_select "ol>li:match('id', ?)", /item-\d+/
-
#
-
# # All input fields in the form have a name
-
# assert_select "form input" do
-
# assert_select ":match('name', ?)", /.+/ # Not empty
-
# end
-
1
def assert_select(*args, &block)
-
@selected ||= nil
-
-
selector = HTMLSelector.new(args, @selected) { nodeset document_root_element }
-
-
if selector.selecting_no_body?
-
assert true
-
return
-
end
-
-
selector.select.tap do |matches|
-
assert_size_match!(matches.size, selector.tests,
-
selector.css_selector, selector.message)
-
-
nest_selection(matches, &block) if block_given? && !matches.empty?
-
end
-
end
-
-
# Extracts the content of an element, treats it as encoded HTML and runs
-
# nested assertion on it.
-
#
-
# You typically call this method within another assertion to operate on
-
# all currently selected elements. You can also pass an element or array
-
# of elements.
-
#
-
# The content of each element is un-encoded, and wrapped in the root
-
# element +encoded+. It then calls the block with all un-encoded elements.
-
#
-
# # Selects all bold tags from within the title of an Atom feed's entries (perhaps to nab a section name prefix)
-
# assert_select "feed[xmlns='http://www.w3.org/2005/Atom']" do
-
# # Select each entry item and then the title item
-
# assert_select "entry>title" do
-
# # Run assertions on the encoded title elements
-
# assert_select_encoded do
-
# assert_select "b"
-
# end
-
# end
-
# end
-
#
-
#
-
# # Selects all paragraph tags from within the description of an RSS feed
-
# assert_select "rss[version=2.0]" do
-
# # Select description element of each feed item.
-
# assert_select "channel>item>description" do
-
# # Run assertions on the encoded elements.
-
# assert_select_encoded do
-
# assert_select "p"
-
# end
-
# end
-
# end
-
1
def assert_select_encoded(element = nil, &block)
-
if !element && !@selected
-
raise ArgumentError, "Element is required when called from a nonnested assert_select"
-
end
-
-
content = nodeset(element || @selected).map do |elem|
-
elem.children.select do |child|
-
child.cdata? || (child.text? && !child.blank?)
-
end.map(&:content)
-
end.join
-
-
selected = Nokogiri::HTML::DocumentFragment.parse(content)
-
nest_selection(selected) do
-
if content.empty?
-
yield selected
-
else
-
assert_select ":root", &block
-
end
-
end
-
end
-
-
# Extracts the body of an email and runs nested assertions on it.
-
#
-
# You must enable deliveries for this assertion to work, use:
-
# ActionMailer::Base.perform_deliveries = true
-
#
-
# assert_select_email do
-
# assert_select "h1", "Email alert"
-
# end
-
#
-
# assert_select_email do
-
# items = assert_select "ol>li"
-
# items.each do
-
# # Work with items here...
-
# end
-
# end
-
1
def assert_select_email(&block)
-
deliveries = ActionMailer::Base.deliveries
-
assert !deliveries.empty?, "No e-mail in delivery list"
-
-
deliveries.each do |delivery|
-
(delivery.parts.empty? ? [delivery] : delivery.parts).each do |part|
-
if part["Content-Type"].to_s =~ /^text\/html\W/
-
root = Nokogiri::HTML::DocumentFragment.parse(part.body.to_s)
-
assert_select root, ":root", &block
-
end
-
end
-
end
-
end
-
-
1
private
-
1
include CountDescribable
-
-
1
def document_root_element
-
raise NotImplementedError, 'Implementing document_root_element makes ' \
-
'assert_select work without needing to specify an element to select from.'
-
end
-
-
# +equals+ must contain :minimum, :maximum and :count keys
-
1
def assert_size_match!(size, equals, css_selector, message = nil)
-
min, max, count = equals[:minimum], equals[:maximum], equals[:count]
-
-
message ||= %(Expected #{count_description(min, max, count)} matching "#{css_selector}", found #{size}.)
-
if count
-
assert_equal count, size, message
-
else
-
assert_operator size, :>=, min, message if min
-
assert_operator size, :<=, max, message if max
-
end
-
end
-
-
1
def nest_selection(selection)
-
# Set @selected to allow nested assert_select.
-
# Can be nested several levels deep.
-
old_selected, @selected = @selected, selection
-
yield @selected
-
ensure
-
@selected = old_selected
-
end
-
-
1
def nodeset(node)
-
if node.is_a?(Nokogiri::XML::NodeSet)
-
node
-
else
-
Nokogiri::XML::NodeSet.new(node.document, [node])
-
end
-
end
-
end
-
end
-
end
-
end
-
end
-
1
require 'active_support/concern'
-
-
1
module Rails
-
1
module Dom
-
1
module Testing
-
1
module Assertions
-
1
module SelectorAssertions
-
1
module CountDescribable
-
1
extend ActiveSupport::Concern
-
-
1
private
-
1
def count_description(min, max, count) #:nodoc:
-
if min && max && (max != min)
-
"between #{min} and #{max} elements"
-
elsif min && max && max == min && count
-
"exactly #{count} #{pluralize_element(min)}"
-
elsif min && !(min == 1 && max == 1)
-
"at least #{min} #{pluralize_element(min)}"
-
elsif max
-
"at most #{max} #{pluralize_element(max)}"
-
end
-
end
-
-
1
def pluralize_element(quantity)
-
quantity == 1 ? 'element' : 'elements'
-
end
-
end
-
end
-
end
-
end
-
end
-
end
-
1
require 'active_support/core_ext/module/attribute_accessors'
-
1
require_relative 'substitution_context'
-
-
1
class HTMLSelector #:nodoc:
-
1
attr_reader :css_selector, :tests, :message
-
-
1
def initialize(values, previous_selection = nil, &root_fallback)
-
@values = values
-
@root = extract_root(previous_selection, root_fallback)
-
extract_selectors
-
@tests = extract_equality_tests
-
@message = @values.shift
-
-
if @values.shift
-
raise ArgumentError, "Not expecting that last argument, you either have too many arguments, or they're the wrong type"
-
end
-
end
-
-
1
def selecting_no_body? #:nodoc:
-
# Nokogiri gives the document a body element. Which means we can't
-
# run an assertion expecting there to not be a body.
-
@selector == 'body' && @tests[:count] == 0
-
end
-
-
1
def select
-
filter @root.css(@selector, context)
-
end
-
-
1
private
-
-
1
NO_STRIP = %w{pre script style textarea}
-
-
2
mattr_reader(:context) { SubstitutionContext.new }
-
-
1
def filter(matches)
-
match_with = tests[:text] || tests[:html]
-
return matches if matches.empty? || !match_with
-
-
content_mismatch = nil
-
text_matches = tests.has_key?(:text)
-
regex_matching = match_with.is_a?(Regexp)
-
-
remaining = matches.reject do |match|
-
# Preserve markup with to_s for html elements
-
content = text_matches ? match.text : match.children.to_s
-
-
content.strip! unless NO_STRIP.include?(match.name)
-
content.sub!(/\A\n/, '') if text_matches && match.name == "textarea"
-
-
next if regex_matching ? (content =~ match_with) : (content == match_with)
-
content_mismatch ||= sprintf("<%s> expected but was\n<%s>.", match_with, content)
-
true
-
end
-
-
@message ||= content_mismatch if remaining.empty?
-
Nokogiri::XML::NodeSet.new(matches.document, remaining)
-
end
-
-
1
def extract_root(previous_selection, root_fallback)
-
possible_root = @values.first
-
-
if possible_root == nil
-
raise ArgumentError, 'First argument is either selector or element ' \
-
'to select, but nil found. Perhaps you called assert_select with ' \
-
'an element that does not exist?'
-
elsif possible_root.respond_to?(:css)
-
@values.shift # remove the root, so selector is the first argument
-
possible_root
-
elsif previous_selection
-
previous_selection
-
else
-
root_fallback.call
-
end
-
end
-
-
1
def extract_selectors
-
selector = @values.shift
-
-
unless selector.is_a? String
-
raise ArgumentError, "Expecting a selector as the first argument"
-
end
-
-
@css_selector = context.substitute!(selector, @values.dup, true)
-
@selector = context.substitute!(selector, @values)
-
end
-
-
1
def extract_equality_tests
-
comparisons = {}
-
case comparator = @values.shift
-
when Hash
-
comparisons = comparator
-
when String, Regexp
-
comparisons[:text] = comparator
-
when Integer
-
comparisons[:count] = comparator
-
when Range
-
comparisons[:minimum] = comparator.begin
-
comparisons[:maximum] = comparator.end
-
when FalseClass
-
comparisons[:count] = 0
-
when NilClass, TrueClass
-
comparisons[:minimum] = 1
-
else raise ArgumentError, "I don't understand what you're trying to match"
-
end
-
-
# By default we're looking for at least one match.
-
if comparisons[:count]
-
comparisons[:minimum] = comparisons[:maximum] = comparisons[:count]
-
else
-
comparisons[:minimum] ||= 1
-
end
-
comparisons
-
end
-
end
-
1
class SubstitutionContext
-
1
def initialize
-
1
@substitute = '?'
-
end
-
-
1
def substitute!(selector, values, format_for_presentation = false)
-
selector = selector.dup
-
-
while !values.empty? && substitutable?(values.first) && selector.index(@substitute)
-
selector.sub! @substitute, matcher_for(values.shift, format_for_presentation)
-
end
-
-
selector
-
end
-
-
1
def match(matches, attribute, matcher)
-
matches.find_all { |node| node[attribute] =~ Regexp.new(matcher) }
-
end
-
-
1
private
-
1
def matcher_for(value, format_for_presentation)
-
# Nokogiri doesn't like arbitrary values without quotes, hence inspect.
-
if format_for_presentation
-
value.inspect # Avoid to_s so Regexps aren't put in quotes.
-
else
-
value.to_s.inspect
-
end
-
end
-
-
1
def substitutable?(value)
-
value.is_a?(String) || value.is_a?(Regexp)
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_support/core_ext/module/attribute_accessors"
-
1
require "rails/test_unit/reporter"
-
1
require "rails/test_unit/runner"
-
-
1
module Minitest
-
1
class SuppressedSummaryReporter < SummaryReporter
-
# Disable extra failure output after a run if output is inline.
-
1
def aggregated_results(*)
-
1
super unless options[:output_inline]
-
end
-
end
-
-
1
def self.plugin_rails_options(opts, options)
-
1
::Rails::TestUnit::Runner.attach_before_load_options(opts)
-
-
1
opts.on("-b", "--backtrace", "Show the complete backtrace") do
-
options[:full_backtrace] = true
-
end
-
-
1
opts.on("-d", "--defer-output", "Output test failures and errors after the test run") do
-
options[:output_inline] = false
-
end
-
-
1
opts.on("-f", "--fail-fast", "Abort test run on first failure or error") do
-
options[:fail_fast] = true
-
end
-
-
1
opts.on("-c", "--[no-]color", "Enable color in the output") do |value|
-
options[:color] = value
-
end
-
-
1
options[:color] = true
-
1
options[:output_inline] = true
-
end
-
-
# Owes great inspiration to test runner trailblazers like RSpec,
-
# minitest-reporters, maxitest and others.
-
1
def self.plugin_rails_init(options)
-
1
unless options[:full_backtrace] || ENV["BACKTRACE"]
-
# Plugin can run without Rails loaded, check before filtering.
-
1
Minitest.backtrace_filter = ::Rails.backtrace_cleaner if ::Rails.respond_to?(:backtrace_cleaner)
-
end
-
-
1
self.plugin_rails_replace_reporters(reporter, options)
-
end
-
-
1
def self.plugin_rails_replace_reporters(minitest_reporter, options)
-
1
return unless minitest_reporter.kind_of?(Minitest::CompositeReporter)
-
-
# Replace progress reporter for colors.
-
3
if minitest_reporter.reporters.reject! { |reporter| reporter.kind_of?(SummaryReporter) } != nil
-
1
minitest_reporter << SuppressedSummaryReporter.new(options[:io], options)
-
end
-
3
if minitest_reporter.reporters.reject! { |reporter| reporter.kind_of?(ProgressReporter) } != nil
-
1
minitest_reporter << ::Rails::TestUnitReporter.new(options[:io], options)
-
end
-
end
-
-
# Backwardscompatibility with Rails 5.0 generated plugin test scripts
-
1
mattr_reader :run_via, default: {}
-
end
-
# frozen_string_literal: true
-
-
1
require "fileutils"
-
1
require "active_support/notifications"
-
1
require "active_support/dependencies"
-
1
require "active_support/descendants_tracker"
-
1
require "rails/secrets"
-
-
1
module Rails
-
1
class Application
-
1
module Bootstrap
-
1
include Initializable
-
-
1
initializer :load_environment_hook, group: :all do end
-
-
1
initializer :load_active_support, group: :all do
-
1
require "active_support/all" unless config.active_support.bare
-
end
-
-
1
initializer :set_eager_load, group: :all do
-
1
if config.eager_load.nil?
-
warn <<-INFO
-
config.eager_load is set to nil. Please update your config/environments/*.rb files accordingly:
-
-
* development - set it to false
-
* test - set it to false (unless you use a tool that preloads your test environment)
-
* production - set it to true
-
-
INFO
-
config.eager_load = config.cache_classes
-
end
-
end
-
-
# Initialize the logger early in the stack in case we need to log some deprecation.
-
1
initializer :initialize_logger, group: :all do
-
1
Rails.logger ||= config.logger || begin
-
1
path = config.paths["log"].first
-
1
unless File.exist? File.dirname path
-
FileUtils.mkdir_p File.dirname path
-
end
-
-
1
f = File.open path, "a"
-
1
f.binmode
-
1
f.sync = config.autoflush_log # if true make sure every write flushes
-
-
1
logger = ActiveSupport::Logger.new f
-
1
logger.formatter = config.log_formatter
-
1
logger = ActiveSupport::TaggedLogging.new(logger)
-
1
logger
-
rescue StandardError
-
logger = ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(STDERR))
-
logger.level = ActiveSupport::Logger::WARN
-
logger.warn(
-
"Rails Error: Unable to access log file. Please ensure that #{path} exists and is writable " \
-
"(ie, make it writable for user and group: chmod 0664 #{path}). " \
-
"The log level has been raised to WARN and the output directed to STDERR until the problem is fixed."
-
)
-
logger
-
end
-
-
1
Rails.logger.level = ActiveSupport::Logger.const_get(config.log_level.to_s.upcase)
-
end
-
-
# Initialize cache early in the stack so railties can make use of it.
-
1
initializer :initialize_cache, group: :all do
-
1
unless Rails.cache
-
1
Rails.cache = ActiveSupport::Cache.lookup_store(config.cache_store)
-
-
1
if Rails.cache.respond_to?(:middleware)
-
1
config.middleware.insert_before(::Rack::Runtime, Rails.cache.middleware)
-
end
-
end
-
end
-
-
# Sets the dependency loading mechanism.
-
1
initializer :initialize_dependency_mechanism, group: :all do
-
1
ActiveSupport::Dependencies.mechanism = config.cache_classes ? :require : :load
-
end
-
-
1
initializer :bootstrap_hook, group: :all do |app|
-
1
ActiveSupport.run_load_hooks(:before_initialize, app)
-
end
-
-
1
initializer :set_secrets_root, group: :all do
-
1
Rails::Secrets.root = root
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Rails
-
1
class Application
-
1
class DefaultMiddlewareStack
-
1
attr_reader :config, :paths, :app
-
-
1
def initialize(app, config, paths)
-
1
@app = app
-
1
@config = config
-
1
@paths = paths
-
end
-
-
1
def build_stack
-
1
ActionDispatch::MiddlewareStack.new do |middleware|
-
1
if config.force_ssl
-
middleware.use ::ActionDispatch::SSL, config.ssl_options
-
end
-
-
1
middleware.use ::Rack::Sendfile, config.action_dispatch.x_sendfile_header
-
-
1
if config.public_file_server.enabled
-
1
headers = config.public_file_server.headers || {}
-
-
1
middleware.use ::ActionDispatch::Static, paths["public"].first, index: config.public_file_server.index_name, headers: headers
-
end
-
-
1
if rack_cache = load_rack_cache
-
require "action_dispatch/http/rack_cache"
-
middleware.use ::Rack::Cache, rack_cache
-
end
-
-
1
if config.allow_concurrency == false
-
# User has explicitly opted out of concurrent request
-
# handling: presumably their code is not threadsafe
-
-
middleware.use ::Rack::Lock
-
end
-
-
1
middleware.use ::ActionDispatch::Executor, app.executor
-
-
1
middleware.use ::Rack::Runtime
-
1
middleware.use ::Rack::MethodOverride unless config.api_only
-
1
middleware.use ::ActionDispatch::RequestId
-
1
middleware.use ::ActionDispatch::RemoteIp, config.action_dispatch.ip_spoofing_check, config.action_dispatch.trusted_proxies
-
-
1
middleware.use ::Rails::Rack::Logger, config.log_tags
-
1
middleware.use ::ActionDispatch::ShowExceptions, show_exceptions_app
-
1
middleware.use ::ActionDispatch::DebugExceptions, app, config.debug_exception_response_format
-
-
1
unless config.cache_classes
-
1
middleware.use ::ActionDispatch::Reloader, app.reloader
-
end
-
-
1
middleware.use ::ActionDispatch::Callbacks
-
1
middleware.use ::ActionDispatch::Cookies unless config.api_only
-
-
1
if !config.api_only && config.session_store
-
1
if config.force_ssl && config.ssl_options.fetch(:secure_cookies, true) && !config.session_options.key?(:secure)
-
config.session_options[:secure] = true
-
end
-
1
middleware.use config.session_store, config.session_options
-
1
middleware.use ::ActionDispatch::Flash
-
end
-
-
1
unless config.api_only
-
1
middleware.use ::ActionDispatch::ContentSecurityPolicy::Middleware
-
end
-
-
1
middleware.use ::Rack::Head
-
1
middleware.use ::Rack::ConditionalGet
-
1
middleware.use ::Rack::ETag, "no-cache"
-
-
1
middleware.use ::Rack::TempfileReaper unless config.api_only
-
end
-
end
-
-
1
private
-
-
1
def load_rack_cache
-
1
rack_cache = config.action_dispatch.rack_cache
-
1
return unless rack_cache
-
-
begin
-
require "rack/cache"
-
rescue LoadError => error
-
error.message << " Be sure to add rack-cache to your Gemfile"
-
raise
-
end
-
-
if rack_cache == true
-
{
-
metastore: "rails:/",
-
entitystore: "rails:/",
-
verbose: false
-
}
-
else
-
rack_cache
-
end
-
end
-
-
1
def show_exceptions_app
-
1
config.exceptions_app || ActionDispatch::PublicExceptions.new(Rails.public_path)
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Rails
-
1
class Application
-
1
module Finisher
-
1
include Initializable
-
-
1
initializer :add_generator_templates do
-
1
config.generators.templates.unshift(*paths["lib/templates"].existent)
-
end
-
-
1
initializer :ensure_autoload_once_paths_as_subset do
-
2
extra = ActiveSupport::Dependencies.autoload_once_paths -
-
ActiveSupport::Dependencies.autoload_paths
-
-
1
unless extra.empty?
-
abort <<-end_error
-
autoload_once_paths must be a subset of the autoload_paths.
-
Extra items in autoload_once_paths: #{extra * ','}
-
end_error
-
end
-
end
-
-
1
initializer :add_builtin_route do |app|
-
1
if Rails.env.development?
-
1
app.routes.prepend do
-
1
get "/rails/info/properties" => "rails/info#properties", internal: true
-
1
get "/rails/info/routes" => "rails/info#routes", internal: true
-
1
get "/rails/info" => "rails/info#index", internal: true
-
end
-
-
1
app.routes.append do
-
1
get "/" => "rails/welcome#index", internal: true
-
end
-
end
-
end
-
-
# Setup default session store if not already set in config/application.rb
-
1
initializer :setup_default_session_store, before: :build_middleware_stack do |app|
-
1
unless app.config.session_store?
-
1
app_name = app.class.name ? app.railtie_name.chomp("_application") : ""
-
1
app.config.session_store :cookie_store, key: "_#{app_name}_session"
-
end
-
end
-
-
1
initializer :build_middleware_stack do
-
1
build_middleware_stack
-
end
-
-
1
initializer :define_main_app_helper do |app|
-
1
app.routes.define_mounted_helper(:main_app)
-
end
-
-
1
initializer :add_to_prepare_blocks do |app|
-
1
config.to_prepare_blocks.each do |block|
-
app.reloader.to_prepare(&block)
-
end
-
end
-
-
# This needs to happen before eager load so it happens
-
# in exactly the same point regardless of config.eager_load
-
1
initializer :run_prepare_callbacks do |app|
-
1
app.reloader.prepare!
-
end
-
-
1
initializer :eager_load! do
-
1
if config.eager_load
-
ActiveSupport.run_load_hooks(:before_eager_load, self)
-
config.eager_load_namespaces.each(&:eager_load!)
-
end
-
end
-
-
# All initialization is done, including eager loading in production
-
1
initializer :finisher_hook do
-
1
ActiveSupport.run_load_hooks(:after_initialize, self)
-
end
-
-
1
class MutexHook
-
1
def initialize(mutex = Mutex.new)
-
@mutex = mutex
-
end
-
-
1
def run
-
@mutex.lock
-
end
-
-
1
def complete(_state)
-
@mutex.unlock
-
end
-
end
-
-
1
module InterlockHook
-
1
def self.run
-
2
ActiveSupport::Dependencies.interlock.start_running
-
end
-
-
1
def self.complete(_state)
-
2
ActiveSupport::Dependencies.interlock.done_running
-
end
-
end
-
-
1
initializer :configure_executor_for_concurrency do |app|
-
1
if config.allow_concurrency == false
-
# User has explicitly opted out of concurrent request
-
# handling: presumably their code is not threadsafe
-
-
app.executor.register_hook(MutexHook.new, outer: true)
-
-
elsif config.allow_concurrency == :unsafe
-
# Do nothing, even if we know this is dangerous. This is the
-
# historical behavior for true.
-
-
else
-
# Default concurrency setting: enabled, but safe
-
-
1
unless config.cache_classes && config.eager_load
-
# Without cache_classes + eager_load, the load interlock
-
# is required for proper operation
-
-
1
app.executor.register_hook(InterlockHook, outer: true)
-
end
-
end
-
end
-
-
# Set routes reload after the finisher hook to ensure routes added in
-
# the hook are taken into account.
-
1
initializer :set_routes_reloader_hook do |app|
-
1
reloader = routes_reloader
-
1
reloader.eager_load = app.config.eager_load
-
1
reloader.execute
-
1
reloaders << reloader
-
1
app.reloader.to_run do
-
# We configure #execute rather than #execute_if_updated because if
-
# autoloaded constants are cleared we need to reload routes also in
-
# case any was used there, as in
-
#
-
# mount MailPreview => 'mail_view'
-
#
-
# This means routes are also reloaded if i18n is updated, which
-
# might not be necessary, but in order to be more precise we need
-
# some sort of reloaders dependency support, to be added.
-
require_unload_lock!
-
reloader.execute
-
end
-
end
-
-
# Set clearing dependencies after the finisher hook to ensure paths
-
# added in the hook are taken into account.
-
1
initializer :set_clear_dependencies_hook, group: :all do |app|
-
1
callback = lambda do
-
ActiveSupport::DescendantsTracker.clear
-
ActiveSupport::Dependencies.clear
-
end
-
-
1
if config.cache_classes
-
app.reloader.check = lambda { false }
-
elsif config.reload_classes_only_on_change
-
1
app.reloader.check = lambda do
-
2
app.reloaders.map(&:updated?).any?
-
end
-
else
-
app.reloader.check = lambda { true }
-
end
-
-
1
if config.reload_classes_only_on_change
-
1
reloader = config.file_watcher.new(*watchable_args, &callback)
-
1
reloaders << reloader
-
-
# Prepend this callback to have autoloaded constants cleared before
-
# any other possible reloading, in case they need to autoload fresh
-
# constants.
-
1
app.reloader.to_run(prepend: true) do
-
# In addition to changes detected by the file watcher, if routes
-
# or i18n have been updated we also need to clear constants,
-
# that's why we run #execute rather than #execute_if_updated, this
-
# callback has to clear autoloaded constants after any update.
-
class_unload! do
-
reloader.execute
-
end
-
end
-
else
-
app.reloader.to_complete do
-
class_unload!(&callback)
-
end
-
end
-
end
-
-
# Disable dependency loading during request cycle
-
1
initializer :disable_dependency_loading do
-
1
if config.eager_load && config.cache_classes && !config.enable_dependency_loading
-
ActiveSupport::Dependencies.unhook!
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_support/core_ext/module/delegation"
-
-
1
module Rails
-
1
class Application
-
1
class RoutesReloader
-
1
attr_reader :route_sets, :paths
-
1
attr_accessor :eager_load
-
1
delegate :execute_if_updated, :execute, :updated?, to: :updater
-
-
1
def initialize
-
1
@paths = []
-
1
@route_sets = []
-
1
@eager_load = false
-
end
-
-
1
def reload!
-
1
clear!
-
1
load_paths
-
1
finalize!
-
1
route_sets.each(&:eager_load!) if eager_load
-
ensure
-
1
revert
-
end
-
-
1
private
-
-
1
def updater
-
4
@updater ||= ActiveSupport::FileUpdateChecker.new(paths) { reload! }
-
end
-
-
1
def clear!
-
1
route_sets.each do |routes|
-
2
routes.disable_clear_and_finalize = true
-
2
routes.clear!
-
end
-
end
-
-
1
def load_paths
-
3
paths.each { |path| load(path) }
-
end
-
-
1
def finalize!
-
1
route_sets.each(&:finalize!)
-
end
-
-
1
def revert
-
1
route_sets.each do |routes|
-
2
routes.disable_clear_and_finalize = false
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_support/backtrace_cleaner"
-
-
1
module Rails
-
1
class BacktraceCleaner < ActiveSupport::BacktraceCleaner
-
1
APP_DIRS_PATTERN = /^\/?(app|config|lib|test|\(\w*\))/
-
1
RENDER_TEMPLATE_PATTERN = /:in `_render_template_\w*'/
-
1
EMPTY_STRING = "".freeze
-
1
SLASH = "/".freeze
-
1
DOT_SLASH = "./".freeze
-
-
1
def initialize
-
1
super
-
1
@root = "#{Rails.root}/".freeze
-
1
add_filter { |line| line.sub(@root, EMPTY_STRING) }
-
1
add_filter { |line| line.sub(RENDER_TEMPLATE_PATTERN, EMPTY_STRING) }
-
1
add_filter { |line| line.sub(DOT_SLASH, SLASH) } # for tests
-
-
1
add_gem_filters
-
1
add_silencer { |line| !APP_DIRS_PATTERN.match?(line) }
-
end
-
-
1
private
-
1
def add_gem_filters
-
6
gems_paths = (Gem.path | [Gem.default_dir]).map { |p| Regexp.escape(p) }
-
1
return if gems_paths.empty?
-
-
1
gems_regexp = %r{(#{gems_paths.join('|')})/gems/([^/]+)-([\w.]+)/(.*)}
-
1
gems_result = '\2 (\3) \4'.freeze
-
1
add_filter { |line| line.sub(gems_regexp, gems_result) }
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "rails/generators"
-
1
require "rails/generators/testing/behaviour"
-
1
require "rails/generators/testing/setup_and_teardown"
-
1
require "rails/generators/testing/assertions"
-
1
require "fileutils"
-
-
1
module Rails
-
1
module Generators
-
# Disable color in output. Easier to debug.
-
1
no_color!
-
-
# This class provides a TestCase for testing generators. To setup, you need
-
# just to configure the destination and set which generator is being tested:
-
#
-
# class AppGeneratorTest < Rails::Generators::TestCase
-
# tests AppGenerator
-
# destination File.expand_path("../tmp", __dir__)
-
# end
-
#
-
# If you want to ensure your destination root is clean before running each test,
-
# you can set a setup callback:
-
#
-
# class AppGeneratorTest < Rails::Generators::TestCase
-
# tests AppGenerator
-
# destination File.expand_path("../tmp", __dir__)
-
# setup :prepare_destination
-
# end
-
1
class TestCase < ActiveSupport::TestCase
-
1
include Rails::Generators::Testing::Behaviour
-
1
include Rails::Generators::Testing::SetupAndTeardown
-
1
include Rails::Generators::Testing::Assertions
-
1
include FileUtils
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Rails
-
1
module Generators
-
1
module Testing
-
1
module Assertions
-
# Asserts a given file exists. You need to supply an absolute path or a path relative
-
# to the configured destination:
-
#
-
# assert_file "config/environment.rb"
-
#
-
# You can also give extra arguments. If the argument is a regexp, it will check if the
-
# regular expression matches the given file content. If it's a string, it compares the
-
# file with the given string:
-
#
-
# assert_file "config/environment.rb", /initialize/
-
#
-
# Finally, when a block is given, it yields the file content:
-
#
-
# assert_file "app/controllers/products_controller.rb" do |controller|
-
# assert_instance_method :index, controller do |index|
-
# assert_match(/Product\.all/, index)
-
# end
-
# end
-
1
def assert_file(relative, *contents)
-
absolute = File.expand_path(relative, destination_root)
-
assert File.exist?(absolute), "Expected file #{relative.inspect} to exist, but does not"
-
-
read = File.read(absolute) if block_given? || !contents.empty?
-
yield read if block_given?
-
-
contents.each do |content|
-
case content
-
when String
-
assert_equal content, read
-
when Regexp
-
assert_match content, read
-
end
-
end
-
end
-
1
alias :assert_directory :assert_file
-
-
# Asserts a given file does not exist. You need to supply an absolute path or a
-
# path relative to the configured destination:
-
#
-
# assert_no_file "config/random.rb"
-
1
def assert_no_file(relative)
-
absolute = File.expand_path(relative, destination_root)
-
assert !File.exist?(absolute), "Expected file #{relative.inspect} to not exist, but does"
-
end
-
1
alias :assert_no_directory :assert_no_file
-
-
# Asserts a given migration exists. You need to supply an absolute path or a
-
# path relative to the configured destination:
-
#
-
# assert_migration "db/migrate/create_products.rb"
-
#
-
# This method manipulates the given path and tries to find any migration which
-
# matches the migration name. For example, the call above is converted to:
-
#
-
# assert_file "db/migrate/003_create_products.rb"
-
#
-
# Consequently, assert_migration accepts the same arguments has assert_file.
-
1
def assert_migration(relative, *contents, &block)
-
file_name = migration_file_name(relative)
-
assert file_name, "Expected migration #{relative} to exist, but was not found"
-
assert_file file_name, *contents, &block
-
end
-
-
# Asserts a given migration does not exist. You need to supply an absolute path or a
-
# path relative to the configured destination:
-
#
-
# assert_no_migration "db/migrate/create_products.rb"
-
1
def assert_no_migration(relative)
-
file_name = migration_file_name(relative)
-
assert_nil file_name, "Expected migration #{relative} to not exist, but found #{file_name}"
-
end
-
-
# Asserts the given class method exists in the given content. This method does not detect
-
# class methods inside (class << self), only class methods which starts with "self.".
-
# When a block is given, it yields the content of the method.
-
#
-
# assert_migration "db/migrate/create_products.rb" do |migration|
-
# assert_class_method :up, migration do |up|
-
# assert_match(/create_table/, up)
-
# end
-
# end
-
1
def assert_class_method(method, content, &block)
-
assert_instance_method "self.#{method}", content, &block
-
end
-
-
# Asserts the given method exists in the given content. When a block is given,
-
# it yields the content of the method.
-
#
-
# assert_file "app/controllers/products_controller.rb" do |controller|
-
# assert_instance_method :index, controller do |index|
-
# assert_match(/Product\.all/, index)
-
# end
-
# end
-
1
def assert_instance_method(method, content)
-
assert content =~ /(\s+)def #{method}(\(.+\))?(.*?)\n\1end/m, "Expected to have method #{method}"
-
yield $3.strip if block_given?
-
end
-
1
alias :assert_method :assert_instance_method
-
-
# Asserts the given attribute type gets translated to a field type
-
# properly:
-
#
-
# assert_field_type :date, :date_select
-
1
def assert_field_type(attribute_type, field_type)
-
assert_equal(field_type, create_generated_attribute(attribute_type).field_type)
-
end
-
-
# Asserts the given attribute type gets a proper default value:
-
#
-
# assert_field_default_value :string, "MyString"
-
1
def assert_field_default_value(attribute_type, value)
-
if value.nil?
-
assert_nil(create_generated_attribute(attribute_type).default)
-
else
-
assert_equal(value, create_generated_attribute(attribute_type).default)
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_support/core_ext/class/attribute"
-
1
require "active_support/core_ext/module/delegation"
-
1
require "active_support/core_ext/hash/reverse_merge"
-
1
require "active_support/core_ext/kernel/reporting"
-
1
require "active_support/testing/stream"
-
1
require "active_support/concern"
-
1
require "rails/generators"
-
-
1
module Rails
-
1
module Generators
-
1
module Testing
-
1
module Behaviour
-
1
extend ActiveSupport::Concern
-
1
include ActiveSupport::Testing::Stream
-
-
1
included do
-
# Generators frequently change the current path using +FileUtils.cd+.
-
# So we need to store the path at file load and revert back to it after each test.
-
1
class_attribute :current_path, default: File.expand_path(Dir.pwd)
-
1
class_attribute :default_arguments, default: []
-
1
class_attribute :destination_root
-
1
class_attribute :generator_class
-
end
-
-
1
module ClassMethods
-
# Sets which generator should be tested:
-
#
-
# tests AppGenerator
-
1
def tests(klass)
-
self.generator_class = klass
-
end
-
-
# Sets default arguments on generator invocation. This can be overwritten when
-
# invoking it.
-
#
-
# arguments %w(app_name --skip-active-record)
-
1
def arguments(array)
-
self.default_arguments = array
-
end
-
-
# Sets the destination of generator files:
-
#
-
# destination File.expand_path("../tmp", __dir__)
-
1
def destination(path)
-
self.destination_root = path
-
end
-
end
-
-
# Runs the generator configured for this class. The first argument is an array like
-
# command line arguments:
-
#
-
# class AppGeneratorTest < Rails::Generators::TestCase
-
# tests AppGenerator
-
# destination File.expand_path("../tmp", __dir__)
-
# setup :prepare_destination
-
#
-
# test "database.yml is not created when skipping Active Record" do
-
# run_generator %w(myapp --skip-active-record)
-
# assert_no_file "config/database.yml"
-
# end
-
# end
-
#
-
# You can provide a configuration hash as second argument. This method returns the output
-
# printed by the generator.
-
1
def run_generator(args = default_arguments, config = {})
-
capture(:stdout) do
-
args += ["--skip-bundle"] unless args.include? "--dev"
-
generator_class.start(args, config.reverse_merge(destination_root: destination_root))
-
end
-
end
-
-
# Instantiate the generator.
-
1
def generator(args = default_arguments, options = {}, config = {})
-
@generator ||= generator_class.new(args, options, config.reverse_merge(destination_root: destination_root))
-
end
-
-
# Create a Rails::Generators::GeneratedAttribute by supplying the
-
# attribute type and, optionally, the attribute name:
-
#
-
# create_generated_attribute(:string, 'name')
-
1
def create_generated_attribute(attribute_type, name = "test", index = nil)
-
Rails::Generators::GeneratedAttribute.parse([name, attribute_type, index].compact.join(":"))
-
end
-
-
1
private
-
-
1
def destination_root_is_set?
-
raise "You need to configure your Rails::Generators::TestCase destination root." unless destination_root
-
end
-
-
1
def ensure_current_path
-
cd current_path
-
end
-
-
# Clears all files and directories in destination.
-
1
def prepare_destination # :doc:
-
rm_rf(destination_root)
-
mkdir_p(destination_root)
-
end
-
-
1
def migration_file_name(relative)
-
absolute = File.expand_path(relative, destination_root)
-
dirname, file_name = File.dirname(absolute), File.basename(absolute).sub(/\.rb$/, "")
-
Dir.glob("#{dirname}/[0-9]*_*.rb").grep(/\d+_#{file_name}.rb$/).first
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Rails
-
1
module Generators
-
1
module Testing
-
1
module SetupAndTeardown
-
1
def setup # :nodoc:
-
destination_root_is_set?
-
ensure_current_path
-
super
-
end
-
-
1
def teardown # :nodoc:
-
ensure_current_path
-
super
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_support/core_ext/time/conversions"
-
1
require "active_support/core_ext/object/blank"
-
1
require "active_support/log_subscriber"
-
1
require "action_dispatch/http/request"
-
1
require "rack/body_proxy"
-
-
1
module Rails
-
1
module Rack
-
# Sets log tags, logs the request, calls the app, and flushes the logs.
-
#
-
# Log tags (+taggers+) can be an Array containing: methods that the +request+
-
# object responds to, objects that respond to +to_s+ or Proc objects that accept
-
# an instance of the +request+ object.
-
1
class Logger < ActiveSupport::LogSubscriber
-
1
def initialize(app, taggers = nil)
-
1
@app = app
-
1
@taggers = taggers || []
-
end
-
-
1
def call(env)
-
2
request = ActionDispatch::Request.new(env)
-
-
2
if logger.respond_to?(:tagged)
-
4
logger.tagged(compute_tags(request)) { call_app(request, env) }
-
else
-
call_app(request, env)
-
end
-
end
-
-
1
private
-
-
1
def call_app(request, env) # :doc:
-
2
instrumenter = ActiveSupport::Notifications.instrumenter
-
2
instrumenter.start "request.action_dispatch", request: request
-
4
logger.info { started_request_message(request) }
-
2
status, headers, body = @app.call(env)
-
4
body = ::Rack::BodyProxy.new(body) { finish(request) }
-
2
[status, headers, body]
-
rescue Exception
-
finish(request)
-
raise
-
ensure
-
2
ActiveSupport::LogSubscriber.flush_all!
-
end
-
-
# Started GET "/session/new" for 127.0.0.1 at 2012-09-26 14:51:42 -0700
-
1
def started_request_message(request) # :doc:
-
2
'Started %s "%s" for %s at %s' % [
-
1
request.request_method,
-
1
request.filtered_path,
-
1
request.remote_ip,
-
1
Time.now.to_default_s ]
-
end
-
-
1
def compute_tags(request) # :doc:
-
2
@taggers.collect do |tag|
-
case tag
-
when Proc
-
tag.call(request)
-
when Symbol
-
request.send(tag)
-
else
-
tag
-
end
-
end
-
end
-
-
1
def finish(request)
-
2
instrumenter = ActiveSupport::Notifications.instrumenter
-
2
instrumenter.finish "request.action_dispatch", request: request
-
end
-
-
1
def logger
-
6
Rails.logger
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
# Make double-sure the RAILS_ENV is not set to production,
-
# so fixtures aren't loaded into that environment
-
1
abort("Abort testing: Your Rails environment is running in production mode!") if Rails.env.production?
-
-
1
require "active_support/test_case"
-
1
require "action_controller"
-
1
require "action_controller/test_case"
-
1
require "action_dispatch/testing/integration"
-
1
require "rails/generators/test_case"
-
-
1
require "active_support/testing/autorun"
-
-
1
if defined?(ActiveRecord::Base)
-
1
begin
-
1
ActiveRecord::Migration.maintain_test_schema!
-
rescue ActiveRecord::PendingMigrationError => e
-
puts e.to_s.strip
-
exit 1
-
end
-
-
1
module ActiveSupport
-
1
class TestCase
-
1
include ActiveRecord::TestFixtures
-
1
self.fixture_path = "#{Rails.root}/test/fixtures/"
-
1
self.file_fixture_path = fixture_path + "files"
-
end
-
end
-
-
1
ActionDispatch::IntegrationTest.fixture_path = ActiveSupport::TestCase.fixture_path
-
end
-
-
# :enddoc:
-
-
1
class ActionController::TestCase
-
1
def before_setup # :nodoc:
-
@routes = Rails.application.routes
-
super
-
end
-
end
-
-
1
class ActionDispatch::IntegrationTest
-
1
def before_setup # :nodoc:
-
2
@routes = Rails.application.routes
-
2
super
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "active_support/core_ext/class/attribute"
-
1
require "minitest"
-
-
1
module Rails
-
1
class TestUnitReporter < Minitest::StatisticsReporter
-
1
class_attribute :executable, default: "bin/rails test"
-
-
1
def record(result)
-
6
super
-
-
6
if options[:verbose]
-
io.puts color_output(format_line(result), by: result)
-
else
-
6
io.print color_output(result.result_code, by: result)
-
end
-
-
6
if output_inline? && result.failure && (!result.skipped? || options[:verbose])
-
io.puts
-
io.puts
-
io.puts color_output(result, by: result)
-
io.puts
-
io.puts format_rerun_snippet(result)
-
io.puts
-
end
-
-
6
if fail_fast? && result.failure && !result.skipped?
-
raise Interrupt
-
end
-
end
-
-
1
def report
-
1
return if output_inline? || filtered_results.empty?
-
io.puts
-
io.puts "Failed tests:"
-
io.puts
-
io.puts aggregated_results
-
end
-
-
1
def aggregated_results # :nodoc:
-
filtered_results.map { |result| format_rerun_snippet(result) }.join "\n"
-
end
-
-
1
def filtered_results
-
if options[:verbose]
-
results
-
else
-
results.reject(&:skipped?)
-
end
-
end
-
-
1
def relative_path_for(file)
-
file.sub(/^#{app_root}\/?/, "")
-
end
-
-
1
private
-
1
def output_inline?
-
7
options[:output_inline]
-
end
-
-
1
def fail_fast?
-
6
options[:fail_fast]
-
end
-
-
1
def format_line(result)
-
klass = result.respond_to?(:klass) ? result.klass : result.class
-
"%s#%s = %.2f s = %s" % [klass, result.name, result.time, result.result_code]
-
end
-
-
1
def format_rerun_snippet(result)
-
location, line = if result.respond_to?(:source_location)
-
result.source_location
-
else
-
result.method(result.name).source_location
-
end
-
-
"#{executable} #{relative_path_for(location)}:#{line}"
-
end
-
-
1
def app_root
-
@app_root ||=
-
if defined?(ENGINE_ROOT)
-
ENGINE_ROOT
-
elsif Rails.respond_to?(:root)
-
Rails.root
-
end
-
end
-
-
1
def colored_output?
-
6
options[:color] && io.respond_to?(:tty?) && io.tty?
-
end
-
-
1
codes = { red: 31, green: 32, yellow: 33 }
-
COLOR_BY_RESULT_CODE = {
-
"." => codes[:green],
-
"E" => codes[:red],
-
"F" => codes[:red],
-
"S" => codes[:yellow]
-
}
-
-
1
def color_output(string, by:)
-
6
if colored_output?
-
"\e[#{COLOR_BY_RESULT_CODE[by.result_code]}m#{string}\e[0m"
-
else
-
6
string
-
end
-
end
-
end
-
end
-
1
require 'sass'
-
-
1
module Sprockets
-
1
module Autoload
-
1
Sass = ::Sass
-
end
-
end
-
1
require 'fileutils'
-
1
require 'logger'
-
1
require 'sprockets/encoding_utils'
-
1
require 'sprockets/path_utils'
-
1
require 'zlib'
-
-
1
module Sprockets
-
1
class Cache
-
# Public: A file system cache store that automatically cleans up old keys.
-
#
-
# Assign the instance to the Environment#cache.
-
#
-
# environment.cache = Sprockets::Cache::FileStore.new("/tmp")
-
#
-
# See Also
-
#
-
# ActiveSupport::Cache::FileStore
-
#
-
1
class FileStore
-
# Internal: Default key limit for store.
-
1
DEFAULT_MAX_SIZE = 25 * 1024 * 1024
-
-
# Internal: Default standard error fatal logger.
-
#
-
# Returns a Logger.
-
1
def self.default_logger
-
logger = Logger.new($stderr)
-
logger.level = Logger::FATAL
-
logger
-
end
-
-
# Public: Initialize the cache store.
-
#
-
# root - A String path to a directory to persist cached values to.
-
# max_size - A Integer of the maximum number of keys the store will hold.
-
# (default: 1000).
-
1
def initialize(root, max_size = DEFAULT_MAX_SIZE, logger = self.class.default_logger)
-
1
@root = root
-
1
@max_size = max_size
-
1
@gc_size = max_size * 0.75
-
1
@logger = logger
-
end
-
-
# Public: Retrieve value from cache.
-
#
-
# This API should not be used directly, but via the Cache wrapper API.
-
#
-
# key - String cache key.
-
#
-
# Returns Object or nil or the value is not set.
-
1
def get(key)
-
path = File.join(@root, "#{key}.cache")
-
-
value = safe_open(path) do |f|
-
begin
-
EncodingUtils.unmarshaled_deflated(f.read, Zlib::MAX_WBITS)
-
rescue Exception => e
-
@logger.error do
-
"#{self.class}[#{path}] could not be unmarshaled: " +
-
"#{e.class}: #{e.message}"
-
end
-
nil
-
end
-
end
-
-
if value
-
FileUtils.touch(path)
-
value
-
end
-
end
-
-
# Public: Set a key and value in the cache.
-
#
-
# This API should not be used directly, but via the Cache wrapper API.
-
#
-
# key - String cache key.
-
# value - Object value.
-
#
-
# Returns Object value.
-
1
def set(key, value)
-
path = File.join(@root, "#{key}.cache")
-
-
# Ensure directory exists
-
FileUtils.mkdir_p File.dirname(path)
-
-
# Check if cache exists before writing
-
exists = File.exist?(path)
-
-
# Serialize value
-
marshaled = Marshal.dump(value)
-
-
# Compress if larger than 4KB
-
if marshaled.bytesize > 4 * 1024
-
deflater = Zlib::Deflate.new(
-
Zlib::BEST_COMPRESSION,
-
Zlib::MAX_WBITS,
-
Zlib::MAX_MEM_LEVEL,
-
Zlib::DEFAULT_STRATEGY
-
)
-
deflater << marshaled
-
raw = deflater.finish
-
else
-
raw = marshaled
-
end
-
-
# Write data
-
PathUtils.atomic_write(path) do |f|
-
f.write(raw)
-
@size = size + f.size unless exists
-
end
-
-
# GC if necessary
-
gc! if size > @max_size
-
-
value
-
end
-
-
# Public: Pretty inspect
-
#
-
# Returns String.
-
1
def inspect
-
"#<#{self.class} size=#{size}/#{@max_size}>"
-
end
-
-
1
private
-
# Internal: Get all cache files along with stats.
-
#
-
# Returns an Array of [String filename, File::Stat] pairs sorted by
-
# mtime.
-
1
def find_caches
-
Dir.glob(File.join(@root, '**/*.cache')).reduce([]) { |stats, filename|
-
stat = safe_stat(filename)
-
# stat maybe nil if file was removed between the time we called
-
# dir.glob and the next stat
-
stats << [filename, stat] if stat
-
stats
-
}.sort_by { |_, stat| stat.mtime.to_i }
-
end
-
-
1
def size
-
@size ||= compute_size(find_caches)
-
end
-
-
1
def compute_size(caches)
-
caches.inject(0) { |sum, (_, stat)| sum + stat.size }
-
end
-
-
1
def safe_stat(fn)
-
File.stat(fn)
-
rescue Errno::ENOENT
-
nil
-
end
-
-
1
def safe_open(path, &block)
-
if File.exist?(path)
-
File.open(path, 'rb', &block)
-
end
-
rescue Errno::ENOENT
-
end
-
-
1
def gc!
-
start_time = Time.now
-
-
caches = find_caches
-
size = compute_size(caches)
-
-
delete_caches, keep_caches = caches.partition { |filename, stat|
-
deleted = size > @gc_size
-
size -= stat.size
-
deleted
-
}
-
-
return if delete_caches.empty?
-
-
FileUtils.remove(delete_caches.map(&:first), force: true)
-
@size = compute_size(keep_caches)
-
-
@logger.warn do
-
secs = Time.now.to_f - start_time.to_f
-
"#{self.class}[#{@root}] garbage collected " +
-
"#{delete_caches.size} files (#{(secs * 1000).to_i}ms)"
-
end
-
end
-
end
-
end
-
end
-
1
class Thor
-
1
module Shell
-
1
class Basic
-
1
DEFAULT_TERMINAL_WIDTH = 80
-
-
1
attr_accessor :base
-
1
attr_reader :padding
-
-
# Initialize base, mute and padding to nil.
-
#
-
1
def initialize #:nodoc:
-
@base = nil
-
@mute = false
-
@padding = 0
-
@always_force = false
-
end
-
-
# Mute everything that's inside given block
-
#
-
1
def mute
-
@mute = true
-
yield
-
ensure
-
@mute = false
-
end
-
-
# Check if base is muted
-
#
-
1
def mute?
-
@mute
-
end
-
-
# Sets the output padding, not allowing less than zero values.
-
#
-
1
def padding=(value)
-
@padding = [0, value].max
-
end
-
-
# Sets the output padding while executing a block and resets it.
-
#
-
1
def indent(count = 1)
-
orig_padding = padding
-
self.padding = padding + count
-
yield
-
self.padding = orig_padding
-
end
-
-
# Asks something to the user and receives a response.
-
#
-
# If a default value is specified it will be presented to the user
-
# and allows them to select that value with an empty response. This
-
# option is ignored when limited answers are supplied.
-
#
-
# If asked to limit the correct responses, you can pass in an
-
# array of acceptable answers. If one of those is not supplied,
-
# they will be shown a message stating that one of those answers
-
# must be given and re-asked the question.
-
#
-
# If asking for sensitive information, the :echo option can be set
-
# to false to mask user input from $stdin.
-
#
-
# If the required input is a path, then set the path option to
-
# true. This will enable tab completion for file paths relative
-
# to the current working directory on systems that support
-
# Readline.
-
#
-
# ==== Example
-
# ask("What is your name?")
-
#
-
# ask("What is the planet furthest from the sun?", :default => "Pluto")
-
#
-
# ask("What is your favorite Neopolitan flavor?", :limited_to => ["strawberry", "chocolate", "vanilla"])
-
#
-
# ask("What is your password?", :echo => false)
-
#
-
# ask("Where should the file be saved?", :path => true)
-
#
-
1
def ask(statement, *args)
-
options = args.last.is_a?(Hash) ? args.pop : {}
-
color = args.first
-
-
if options[:limited_to]
-
ask_filtered(statement, color, options)
-
else
-
ask_simply(statement, color, options)
-
end
-
end
-
-
# Say (print) something to the user. If the sentence ends with a whitespace
-
# or tab character, a new line is not appended (print + flush). Otherwise
-
# are passed straight to puts (behavior got from Highline).
-
#
-
# ==== Example
-
# say("I know you knew that.")
-
#
-
1
def say(message = "", color = nil, force_new_line = (message.to_s !~ /( |\t)\Z/))
-
buffer = prepare_message(message, *color)
-
buffer << "\n" if force_new_line && !message.to_s.end_with?("\n")
-
-
stdout.print(buffer)
-
stdout.flush
-
end
-
-
# Say a status with the given color and appends the message. Since this
-
# method is used frequently by actions, it allows nil or false to be given
-
# in log_status, avoiding the message from being shown. If a Symbol is
-
# given in log_status, it's used as the color.
-
#
-
1
def say_status(status, message, log_status = true)
-
return if quiet? || log_status == false
-
spaces = " " * (padding + 1)
-
color = log_status.is_a?(Symbol) ? log_status : :green
-
-
status = status.to_s.rjust(12)
-
status = set_color status, color, true if color
-
-
buffer = "#{status}#{spaces}#{message}"
-
buffer = "#{buffer}\n" unless buffer.end_with?("\n")
-
-
stdout.print(buffer)
-
stdout.flush
-
end
-
-
# Make a question the to user and returns true if the user replies "y" or
-
# "yes".
-
#
-
1
def yes?(statement, color = nil)
-
!!(ask(statement, color, :add_to_history => false) =~ is?(:yes))
-
end
-
-
# Make a question the to user and returns true if the user replies "n" or
-
# "no".
-
#
-
1
def no?(statement, color = nil)
-
!!(ask(statement, color, :add_to_history => false) =~ is?(:no))
-
end
-
-
# Prints values in columns
-
#
-
# ==== Parameters
-
# Array[String, String, ...]
-
#
-
1
def print_in_columns(array)
-
return if array.empty?
-
colwidth = (array.map { |el| el.to_s.size }.max || 0) + 2
-
array.each_with_index do |value, index|
-
# Don't output trailing spaces when printing the last column
-
if ((((index + 1) % (terminal_width / colwidth))).zero? && !index.zero?) || index + 1 == array.length
-
stdout.puts value
-
else
-
stdout.printf("%-#{colwidth}s", value)
-
end
-
end
-
end
-
-
# Prints a table.
-
#
-
# ==== Parameters
-
# Array[Array[String, String, ...]]
-
#
-
# ==== Options
-
# indent<Integer>:: Indent the first column by indent value.
-
# colwidth<Integer>:: Force the first column to colwidth spaces wide.
-
#
-
1
def print_table(array, options = {}) # rubocop:disable MethodLength
-
return if array.empty?
-
-
formats = []
-
indent = options[:indent].to_i
-
colwidth = options[:colwidth]
-
options[:truncate] = terminal_width if options[:truncate] == true
-
-
formats << "%-#{colwidth + 2}s".dup if colwidth
-
start = colwidth ? 1 : 0
-
-
colcount = array.max { |a, b| a.size <=> b.size }.size
-
-
maximas = []
-
-
start.upto(colcount - 1) do |index|
-
maxima = array.map { |row| row[index] ? row[index].to_s.size : 0 }.max
-
maximas << maxima
-
formats << if index == colcount - 1
-
# Don't output 2 trailing spaces when printing the last column
-
"%-s".dup
-
else
-
"%-#{maxima + 2}s".dup
-
end
-
end
-
-
formats[0] = formats[0].insert(0, " " * indent)
-
formats << "%s"
-
-
array.each do |row|
-
sentence = "".dup
-
-
row.each_with_index do |column, index|
-
maxima = maximas[index]
-
-
f = if column.is_a?(Numeric)
-
if index == row.size - 1
-
# Don't output 2 trailing spaces when printing the last column
-
"%#{maxima}s"
-
else
-
"%#{maxima}s "
-
end
-
else
-
formats[index]
-
end
-
sentence << f % column.to_s
-
end
-
-
sentence = truncate(sentence, options[:truncate]) if options[:truncate]
-
stdout.puts sentence
-
end
-
end
-
-
# Prints a long string, word-wrapping the text to the current width of the
-
# terminal display. Ideal for printing heredocs.
-
#
-
# ==== Parameters
-
# String
-
#
-
# ==== Options
-
# indent<Integer>:: Indent each line of the printed paragraph by indent value.
-
#
-
1
def print_wrapped(message, options = {})
-
indent = options[:indent] || 0
-
width = terminal_width - indent
-
paras = message.split("\n\n")
-
-
paras.map! do |unwrapped|
-
counter = 0
-
unwrapped.split(" ").inject do |memo, word|
-
word = word.gsub(/\n\005/, "\n").gsub(/\005/, "\n")
-
counter = 0 if word.include? "\n"
-
if (counter + word.length + 1) < width
-
memo = "#{memo} #{word}"
-
counter += (word.length + 1)
-
else
-
memo = "#{memo}\n#{word}"
-
counter = word.length
-
end
-
memo
-
end
-
end.compact!
-
-
paras.each do |para|
-
para.split("\n").each do |line|
-
stdout.puts line.insert(0, " " * indent)
-
end
-
stdout.puts unless para == paras.last
-
end
-
end
-
-
# Deals with file collision and returns true if the file should be
-
# overwritten and false otherwise. If a block is given, it uses the block
-
# response as the content for the diff.
-
#
-
# ==== Parameters
-
# destination<String>:: the destination file to solve conflicts
-
# block<Proc>:: an optional block that returns the value to be used in diff and merge
-
#
-
1
def file_collision(destination)
-
return true if @always_force
-
options = block_given? ? "[Ynaqdhm]" : "[Ynaqh]"
-
-
loop do
-
answer = ask(
-
%[Overwrite #{destination}? (enter "h" for help) #{options}],
-
:add_to_history => false
-
)
-
-
case answer
-
when nil
-
say ""
-
return true
-
when is?(:yes), is?(:force), ""
-
return true
-
when is?(:no), is?(:skip)
-
return false
-
when is?(:always)
-
return @always_force = true
-
when is?(:quit)
-
say "Aborting..."
-
raise SystemExit
-
when is?(:diff)
-
show_diff(destination, yield) if block_given?
-
say "Retrying..."
-
when is?(:merge)
-
if block_given? && !merge_tool.empty?
-
merge(destination, yield)
-
return nil
-
end
-
-
say "Please specify merge tool to `THOR_MERGE` env."
-
else
-
say file_collision_help
-
end
-
end
-
end
-
-
# This code was copied from Rake, available under MIT-LICENSE
-
# Copyright (c) 2003, 2004 Jim Weirich
-
1
def terminal_width
-
result = if ENV["THOR_COLUMNS"]
-
ENV["THOR_COLUMNS"].to_i
-
else
-
unix? ? dynamic_width : DEFAULT_TERMINAL_WIDTH
-
end
-
result < 10 ? DEFAULT_TERMINAL_WIDTH : result
-
rescue
-
DEFAULT_TERMINAL_WIDTH
-
end
-
-
# Called if something goes wrong during the execution. This is used by Thor
-
# internally and should not be used inside your scripts. If something went
-
# wrong, you can always raise an exception. If you raise a Thor::Error, it
-
# will be rescued and wrapped in the method below.
-
#
-
1
def error(statement)
-
stderr.puts statement
-
end
-
-
# Apply color to the given string with optional bold. Disabled in the
-
# Thor::Shell::Basic class.
-
#
-
1
def set_color(string, *) #:nodoc:
-
string
-
end
-
-
1
protected
-
-
1
def prepare_message(message, *color)
-
spaces = " " * padding
-
spaces + set_color(message.to_s, *color)
-
end
-
-
1
def can_display_colors?
-
false
-
end
-
-
1
def lookup_color(color)
-
return color unless color.is_a?(Symbol)
-
self.class.const_get(color.to_s.upcase)
-
end
-
-
1
def stdout
-
$stdout
-
end
-
-
1
def stderr
-
$stderr
-
end
-
-
1
def is?(value) #:nodoc:
-
value = value.to_s
-
-
if value.size == 1
-
/\A#{value}\z/i
-
else
-
/\A(#{value}|#{value[0, 1]})\z/i
-
end
-
end
-
-
1
def file_collision_help #:nodoc:
-
<<-HELP
-
Y - yes, overwrite
-
n - no, do not overwrite
-
a - all, overwrite this and all others
-
q - quit, abort
-
d - diff, show the differences between the old and the new
-
h - help, show this help
-
m - merge, run merge tool
-
HELP
-
end
-
-
1
def show_diff(destination, content) #:nodoc:
-
diff_cmd = ENV["THOR_DIFF"] || ENV["RAILS_DIFF"] || "diff -u"
-
-
require "tempfile"
-
Tempfile.open(File.basename(destination), File.dirname(destination)) do |temp|
-
temp.write content
-
temp.rewind
-
system %(#{diff_cmd} "#{destination}" "#{temp.path}")
-
end
-
end
-
-
1
def quiet? #:nodoc:
-
mute? || (base && base.options[:quiet])
-
end
-
-
# Calculate the dynamic width of the terminal
-
1
def dynamic_width
-
@dynamic_width ||= (dynamic_width_stty.nonzero? || dynamic_width_tput)
-
end
-
-
1
def dynamic_width_stty
-
`stty size 2>/dev/null`.split[1].to_i
-
end
-
-
1
def dynamic_width_tput
-
`tput cols 2>/dev/null`.to_i
-
end
-
-
1
def unix?
-
RUBY_PLATFORM =~ /(aix|darwin|linux|(net|free|open)bsd|cygwin|solaris|irix|hpux)/i
-
end
-
-
1
def truncate(string, width)
-
as_unicode do
-
chars = string.chars.to_a
-
if chars.length <= width
-
chars.join
-
else
-
chars[0, width - 3].join + "..."
-
end
-
end
-
end
-
-
1
if "".respond_to?(:encode)
-
1
def as_unicode
-
yield
-
end
-
else
-
def as_unicode
-
old = $KCODE
-
$KCODE = "U"
-
yield
-
ensure
-
$KCODE = old
-
end
-
end
-
-
1
def ask_simply(statement, color, options)
-
default = options[:default]
-
message = [statement, ("(#{default})" if default), nil].uniq.join(" ")
-
message = prepare_message(message, *color)
-
result = Thor::LineEditor.readline(message, options)
-
-
return unless result
-
-
result = result.strip
-
-
if default && result == ""
-
default
-
else
-
result
-
end
-
end
-
-
1
def ask_filtered(statement, color, options)
-
answer_set = options[:limited_to]
-
correct_answer = nil
-
until correct_answer
-
answers = answer_set.join(", ")
-
answer = ask_simply("#{statement} [#{answers}]", color, options)
-
correct_answer = answer_set.include?(answer) ? answer : nil
-
say("Your response must be one of: [#{answers}]. Please try again.") unless correct_answer
-
end
-
correct_answer
-
end
-
-
1
def merge(destination, content) #:nodoc:
-
require "tempfile"
-
Tempfile.open([File.basename(destination), File.extname(destination)], File.dirname(destination)) do |temp|
-
temp.write content
-
temp.rewind
-
system %(#{merge_tool} "#{temp.path}" "#{destination}")
-
end
-
end
-
-
1
def merge_tool #:nodoc:
-
@merge_tool ||= ENV["THOR_MERGE"] || git_merge_tool
-
end
-
-
1
def git_merge_tool #:nodoc:
-
`git config merge.tool`.rstrip rescue ""
-
end
-
end
-
end
-
end
-
# encoding: UTF-8
-
-
# This file contains data derived from the IANA Time Zone Database
-
# (http://www.iana.org/time-zones).
-
-
1
module TZInfo
-
1
module Data
-
1
module Definitions
-
1
module Etc
-
1
module UTC
-
1
include TimezoneDefinition
-
-
1
timezone 'Etc/UTC' do |tz|
-
1
tz.offset :o0, 0, 0, :UTC
-
-
end
-
end
-
end
-
end
-
end
-
end